如何防止TypeScript中的文字类型

时间:2019-05-27 20:50:25

标签: 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>

到目前为止,还不错,但是现在我想将其限制为一组类型中的一个,让我们说一下原语:

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 :(

Playground link

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

我在这里做错了什么?不用自己显式扩展类型(例如:new Primitive<string>('hello')),我能做些什么?

2 个答案:

答案 0 :(得分:2)

TypeScript intentionally infers literal types just about everywhere,但除少数情况外,通常会扩展这些类型。一种是当您拥有extends其中一种扩展类型的类型参数时。启发式方法是,如果您要求输入T extends string,则可能需要保留确切的文字。像T extends Primitives这样的联合也是如此,因此您会得到这种行为。

我们可以使用conditional types强制将字符串,数字和布尔文字的(联合)扩展为stringnumberboolean的(联合):

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中的bug导致无法正确检查条件类型。类型PrimitiveDataTest<"hello">PrimitiveDataTest<"goodbye">彼此互为structurally identical to,并分别为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的用户来说可能更容易使用。


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

Link to code

答案 1 :(得分:1)

这很丑陋,但是您可以重载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