【问题标题】:How to prevent a literal type in TypeScript如何防止 TypeScript 中的文字类型
【发布时间】:2019-10-13 09:33:09
【问题描述】:

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

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&lt;'hello'&gt; 而不是Primitive&lt;string&gt;,所以set 只会将文字'hello' 作为一个值,这显然不是我想要的。

我在这里做错了什么?如果我自己不诉诸明确扩大类型(例如:new Primitive&lt;string&gt;('hello')),我能做些什么吗?

【问题讨论】:

  • 您可以使用conditional type 来加宽文字;如果在我开始之前没有其他人这样做,我可能会写下来

标签: typescript typescript-generics


【解决方案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

【讨论】:

  • 是的,不幸的是,这意味着set 方法可以采用任何类型。我可能没有在问题中说清楚,但set 应该只采用相同类型的值。例如:new Data(1).set('hello'); 应该是一个错误。
  • 罗杰。祝你好运。打字稿中类型的联合方式也让我感到沮丧。
【解决方案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&lt;T&gt; 代替TPrimitiveData 内的任何地方:

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

就目前而言,这是可行的。 bTestPrimitiveDataTest&lt;"hello"&gt; 类型,但 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&lt;"hello"&gt;PrimitiveDataTest&lt;"goodbye"&gt; 彼此是structurally identical toPrimitiveDataTest&lt;string&gt;,所以类型应该是可以相互分配的。它们不是一个错误,在不久的将来可能会或可能不会得到解决(可能为 TS3.5 或 TS3.6 设置了一些修复?)

如果没关系,那么您可能就可以停下来了。


否则,您可能会考虑采用这种实现方式。定义像Data&lt;T&gt;这样的无约束版本:

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

然后像这样定义与Data相关的类型和值PrimitiveData

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

【讨论】:

  • 您肯定会提供“写下来”的提议。 :) 这里有很好的信息和解释,谢谢!
猜你喜欢
  • 1970-01-01
  • 2019-12-29
  • 2019-07-24
  • 2021-02-05
  • 1970-01-01
  • 2022-12-14
  • 1970-01-01
  • 2023-02-02
  • 1970-01-01
相关资源
最近更新 更多