Лучший способ различать возможности универсального типа во время выполнения?

#typescript

#typescript

Вопрос:

У меня есть универсальный тип, который может быть одной из 6 возможностей:

 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[]>;
  

Я хочу различать их во время выполнения. В псевдокоде:

 [..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)
  }
  ...
}
  

Поскольку типы TypeScript не могут быть проверены во время выполнения, мне нужно написать некоторый код JavaScript для имитации типов TypeScript. Я вижу два альтернативных решения

Определяемые пользователем средства защиты типов:

 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)
  }
  ...
}
  

Классы:

 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)
  }
  ...
}
  

На мой вопрос:

  • Я не могу выбирать между определяемым пользователем подходом к защите типов и классовым подходом. Каковы, по вашему мнению, преимущества каждого из них? Можно ли сказать, что один из них лучше или более идиоматичен, чем другой?
  • Есть что-то подозрительное в моем примере? Я неправильно думаю о своей проблеме. Видите ли вы какой-то другой подход, который я должен использовать?

Комментарии:

1. Классовый подход не проверяет элементы массива.

2. @AluanHaddad: Этого не происходит во время выполнения, но это происходит во время компиляции. При использовании классового подхода компилятор ts проверяет, что значение, переданное конструктору, является массивом чисел.

3. Так было бы присвоение, подобное const x: NumberArrayAttribute = {name: 'number array attr 1', value: ['hello']} . Однако вы спросили о проверке во время выполнения, так что это отвлекающий маневр

Ответ №1:

Почему вы хотите различать универсальные типы в своей функции setAttribute? В этом случае каждый раз, когда вы добавляете новый атрибут, вам также необходимо обновлять свою функцию setAttribute.

На мой взгляд, каждый экземпляр атрибута должен знать, как выполнять свою работу по настройке «атрибута».

Рассмотрим этот интерфейс:

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

и этот класс:

 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() amp;amp; this.value.every((v) => typeof v === "number");
  }

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

Класс знает, как выполнять как guard, так и setAttribute.
Теперь вы можете создавать столько классов атрибутов<>, сколько хотите, без изменения функции setAttribute:

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