Best way to differentiate between possibilities of a generic type at runtime?

97 views Asked by At

I have a generic type that can be one of 6 possibilities:

type Attribute<T> = {
  name: string;
  value: T | null;
};

type StringAttribute = Attribute<string>;
type BoolAttribute = Attribute<boolean>;
type NumberAttribute = Attribute<number>;
type NumberArrayAttribute = Attribute<number[]>;
type PointAttribute = Attribute<Point>;
type PointArrayAttribute = Attribute<Point[]>;

I want to differentiate between these at runtime. In pseudo-code:

[..attributes].forEach((attr) => setAttribute(attr, target))

function setAttribute(attr: Attribute<string | boolean | number | number[] | point | point[]>, target: ExtendedElement) {
  ...
  if (isNumberArrayAttribute(attr)) {
   target.setNumberArrayAttribute(attr.name, attr.value)
  }
  ...
}

Since the TypeScript types can't be inspected at rum-time I need to write some JavaScript code to mimic the TypeScript types. I see two alternative solutions

User defined type guards:

function isNumberArray(val: any): val is number[] {
  if (!val.isArray()) {
    return false;
  }
  return val.every((v) => typeof v === "number")
} 

function setAttribute(attr: Attribute<string | boolean | number | number[] | point | point[]>, target: ExtendedElement) {
  ...
  if (isNumberArray(attr.value)) {
    target.setAttr(attr.name, attr.value)
  }
  ...
}

Classes:

class NumberArrayAttribute extends Attribute {
  name: string
  value: number[]
  constructor(name: string, value: number[]) {
    this.name = name;
    this.value= value;
  }
}

function setAttribute(attr: Attribute<string | boolean | number | number[] | point | point[]>, target: ExtendedElement) {
  ...
  if (attr instanceof NumberArrayAttribute) {
    target.setNumberArrayAttribute(attr.name, attr.value)
  }
  ...
}

To my question:

  • I can't choose between the user defined type guards approach and the class approach. What would you say are the advantages of each? Can one said to be better or more idiomatic than the other?
  • Is something fishy about my example? Am I thinking about my problem the wrong way. Do you see some other approach I should be using?
1

There are 1 answers

0
Manuel On

Why do you want to differentiate generic types in your function setAttribute? If you do so, every time you add a new Attribute you also need to update your function setAttribute.

In my opinion, every attribute instance should know how to do their job of setting the "attribute".

Consider this interface:

interface Attribute<T> {
  name: string;
  value: T | null;
  guard(): boolean;
  setAttribute(target: ExtendedElement): void;
};

and this class:

class NumberArrayAttribute implements Attribute<number[]> {
  name: string;
  value: number[];

  constructor(name: string, value: number[]) {
    this.name = name;
    this.value= value;
  }

  public guard(): boolean {
    return this.value.isArray() && this.value.every((v) => typeof v === "number");
  }

  public setAttribute(target: ExtendedElement): void {
    target.setAttribute(this.name, this.value)
  }
}

The class knows how to perform both the guard and the setAttribute. Now you can create as many Attribute<> classes as you want, without changing the function setAttribute:

function setAttribute(){
   [...attributes].forEach((attr: Attribute<any>) => {
       if (attr.guard()){
          attr.setAttribute(target)
       }
   })
}