Как предотвратить литеральный тип в TypeScript - PullRequest
1 голос
/ 27 мая 2019

Допустим, у меня есть такой класс, который оборачивает значение:

class Data<T> {
  constructor(public val: T){}

  set(newVal: T) {
    this.val = newVal;
  }
}

const a = new Data('hello');
a.set('world');
// typeof a --> Primitive<string>

Пока все хорошо, но теперь я хочу ограничить его только одним из набора типов, скажем, примитивов:

type Primitives = boolean|string|number|null|undefined;

class PrimitiveData<T extends Primitives> {
  constructor(public val: T){}

  set(newVal: T) {
    this.val = newVal;
  }
}

const b = new PrimitiveData('hello');
b.set('world'); // Error :(

Ссылка на игровую площадку

Эта последняя строка завершается с ошибкой, потому что b является Primitive<'hello'>, а не Primitive<string>, и поэтому set будет принимать в качестве значения только литерал 'hello', что явно не то, что я ищу.

Что я здесь не так делаю? Не прибегая к явному расширению типов самостоятельно (например: new Primitive<string>('hello')), я могу что-нибудь сделать?

Ответы [ 2 ]

2 голосов
/ 28 мая 2019

TypeScript преднамеренно выводит буквальные типы почти везде , но обычно расширяет эти типы, за исключением нескольких случаев.Один из них - это когда у вас есть параметр типа extends один из расширенных типов.Эвристика заключается в том, что если вы запрашиваете T extends string, вы можете сохранить точное буквальное значение.Это все еще верно для объединений, таких как T extends Primitives, поэтому вы получаете такое поведение.

Мы можем использовать условные типы , чтобы заставить (объединения) строковых, числовых и логических литералов расширятьсяto (союзы) string, number и boolean:

type WidenLiterals<T> = 
  T extends boolean ? boolean :
  T extends string ? string : 
  T extends number ? number : 
  T;

type WString = WidenLiterals<"hello"> // string
type WNumber = WidenLiterals<123> // number
type WBooleanOrUndefined = WidenLiterals<true | undefined> // boolean | undefined

Теперь это здорово, и один из способов, которым вы можете продолжить, - это использовать WidenLiterals<T> вместо T везде внутри PrimitiveData:

class PrimitiveDataTest<T extends Primitives> {
  constructor(public val: WidenLiterals<T>){}
  set(newVal: WidenLiterals<T>) {
    this.val = newVal;
  }
}

const bTest = new PrimitiveDataTest("hello"); // PrimitiveDataTest<"hello">
bTest.set("world"); // okay

И это работает, насколько это возможно.bTest имеет тип PrimitiveDataTest<"hello">, но фактический тип val равен string, и вы можете использовать его как таковой.К сожалению, вы получаете следующее нежелательное поведение:

let aTest = new PrimitiveDataTest("goodbye"); // PrimitiveDataTest<"goodbye">
aTest = bTest; // error! 
// PrimitiveDataTest<"hello"> not assignable to PrimitiveDataTest<"goodbye">.
// Type '"hello"' is not assignable to type '"goodbye"'.

Это, похоже, связано с ошибкой в TypeScript, когда условные типы не проверяются должным образом.Типы PrimitiveDataTest<"hello"> и PrimitiveDataTest<"goodbye"> каждый структурно идентичны друг другу и PrimitiveDataTest<string>, поэтому типы должны назначаться взаимно.То, что это не так, является ошибкой, которая может или не может быть исправлена ​​в ближайшем будущем (может быть, некоторые исправления установлены для TS3.5 или TS3.6?)

Если все в порядке, то вы, вероятно, можете остановиться на этом.


В противном случае, вы можете рассмотреть эту реализацию вместо этого.Определите неограниченную версию, например Data<T>:

class Data<T> {
  constructor(public val: T) {}
  set(newVal: T) {
    this.val = newVal;
  }
}

, а затем определите тип и значение PrimitiveData, относящиеся к Data, например:

interface PrimitiveData<T extends Primitives> extends Data<T> {}
const PrimitiveData = Data as new <T extends Primitives>(
  val: T
) => PrimitiveData<WidenLiterals<T>>;

Пара типови значение с именем PrimitiveData действует как универсальный класс, где T ограничено Primitives, но когда вы вызываете конструктор, результирующий экземпляр имеет расширенный тип:

const b = new PrimitiveData("hello"); // PrimitiveData<string>
b.set("world"); // okay
let a = new PrimitiveData("goodbye"); // PrimitiveData<string>
a = b; // okay

Это может быть прощедля пользователей PrimitiveData для работы, хотя реализация PrimitiveData требует небольшого количества прыжков с обручем.


Хорошо, надеюсь, это поможет вам двигаться вперед.Удачи!

Ссылка на код

1 голос
/ 28 мая 2019

Это очень уродливо, но вы можете перегрузить constructor и удалить generic из имени класса.

class Data {
  constructor(public val: boolean)
  constructor(public val: string)
  constructor(public val: number)
  constructor(public val: null) {}

  set(newVal: boolean)
  set(newVal: string)
  set(newVal: number)
  set(newVal: null) {
    this.val = newVal;
  }
}

https://github.com/Microsoft/TypeScript-Handbook/blob/master/pages/Functions.md#overloads

...