如何防止 TypeScript 中的文字类型

nic*_*ckf 3 typescript typescript-generics

假设我有一个这样的类,它包含一个值:

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>
Run Code Online (Sandbox Code Playgroud)

到目前为止一切顺利,但现在我想将它限制为一组类型中的一个,让我们说原语:

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 :(
Run Code Online (Sandbox Code Playgroud)

游乐场链接

最后一行失败,因为bis aPrimitive<'hello'>不是 a Primitive<string>,因此set只会将文字'hello'作为值,这显然不是我想要的。

我在这里做错了什么?不诉诸明确扩大自己的类型(例如:)new Primitive<string>('hello')有什么我可以做的吗?

jca*_*alz 6

TypeScript有意在任何地方推断文字类型,但通常会扩大这些类型,除非在少数情况下。一种是当您有一个类型参数是其中extends一种扩展类型时。启发式是,如果您要求,T extends string您可能会关心保留确切的文字。对于工会,如T extends Primitives,这仍然是正确的,因此您会得到这种行为。

我们可以使用条件类型强制(的工会)字符串,数字和布尔文字扩大到(的工会)stringnumber以及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
Run Code Online (Sandbox Code Playgroud)

现在这很棒,您可能想要继续的一种方法是使用WidenLiterals<T>in place of Tevery inside 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
Run Code Online (Sandbox Code Playgroud)

就目前而言,这是有效的。bTest是类型的PrimitiveDataTest<"hello">,但实际的类型valstring,你可以使用它作为这样。不幸的是,您会遇到这种不良行为:

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"'.
Run Code Online (Sandbox Code Playgroud)

这似乎是由于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;
  }
}
Run Code Online (Sandbox Code Playgroud)

然后定义类型和值PrimitiveData作为与Data这样的:

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

这对命名的类型和值PrimitiveData就像一个泛型类,其中T被约束为Primitives,但是当您调用构造函数时,生成的实例是扩展类型的:

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

这对于 的用户来说可能更容易PrimitiveData使用,尽管 的实现PrimitiveData确实需要一些跳圈。


好的,希望能帮助你前进。祝你好运!

代码链接