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
требует небольшого количества прыжков с обручем.
Хорошо, надеюсь, это поможет вам двигаться вперед.Удачи!
Ссылка на код