如何防止 TypeScript 中的文字类型 [英] How to prevent a literal type in TypeScript

查看:15
本文介绍了如何防止 TypeScript 中的文字类型的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

Let's say I have a class like this which wraps over a value:

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>

到目前为止一切都很好,但现在我想将它限制为一组类型中的一个,比如说原始类型:

So far so good, but now I want to restrict it to only one of a set of types, let's say the primitives:

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

游乐场链接

最后一行失败,因为 bPrimitive<'hello'> 而不是 Primitive,所以 set 只会将文字 'hello' 作为值,这显然不是我想要的.

That last line fails because b is a Primitive<'hello'> not a Primitive<string>, and so set will only take the literal 'hello' as a value, which clearly isn't what I'm after.

我在这里做错了什么?无需自己显式扩展类型(例如:new Primitive('hello')),我能做些什么吗?

What am I doing wrong here? Without resorting to explicitly widening the types myself (eg: new Primitive<string>('hello')) is there anything I can do?

推荐答案

TypeScript 故意推断文字类型只是大约无处不在,但通常会扩大这些类型,除非在少数情况下.一种是当您有一个 extends 扩展类型之一的类型参数时.启发式是,如果您要求 T extends string,您可能希望保留确切的文字.对于联合,这仍然是正确的,例如 T extends Primitives,因此您会得到这种行为.

TypeScript intentionally infers literal types just about everywhere, but usually widens those types except in a few circumstances. One is when you have a type parameter which extends one of the widened types. The heuristic is that if you're asking for T extends string you might care to keep the exact literal. This is still true with unions, like T extends Primitives, so you get this behavior.

我们可以使用条件类型强制(联合)字符串、数字和布尔文字扩展为(联合)stringnumberboolean:

We can use conditional types to force (unions of) string, number, and boolean literals to widen to (unions of) string, number, and 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

现在这很棒,您可能想要继续的一种方法是在 PrimitiveDataWidenLiterals 代替 T>:

Now this is great, and one way you might want to proceed is to use WidenLiterals<T> in place of T everywhere 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

就目前而言,这是有效的.bTestPrimitiveDataTest<"hello"> 类型,但 val 的实际类型是 string 而你可以这样使用.不幸的是,您会遇到这种不良行为:

And that works as far as it goes. bTest is of type PrimitiveDataTest<"hello">, but the actual type of val is string and you can use it as such. Unfortunately, you get this undesirable behavior:

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,所以类型应该可以相互分配.它们不是一个错误,在不久的将来可能会或可能不会得到解决(也许为 TS3.5 或 TS3.6 设置了一些修复?)

This seems to be due to a bug in TypeScript where conditional types are not being checked properly. The types PrimitiveDataTest<"hello"> and PrimitiveDataTest<"goodbye"> are each structurally identical to each other and to PrimitiveDataTest<string>, so the types should be mutually assignable. That they are not is a bug which may or may not get addressed in the near future (maybe some fixes are set for TS3.5 or TS3.6?)

如果没问题,那么你可能就到此为止了.

If that's okay then you can probably stop there.

否则,您可能会考虑使用此实现.定义一个不受约束的版本,如 Data:

Otherwise, you might consider this implementation, instead. Define an unconstrained version like Data<T>:

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

然后定义类型和值 PrimitiveDataData 相关,如下所示:

And then define the type and value PrimitiveData as related to Data like this:

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

名为 PrimitiveData 的类型和值对就像一个泛型类,其中 T 被约束为 Primitives,但是当你调用构造函数时结果实例是加宽类型的:

The pair of type and value named PrimitiveData acts like a generic class where T is constrained to Primitives, but when you call the constructor the resulting instance is of the widened type:

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

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

That might be easier for users of PrimitiveData to work with, although the implementation of PrimitiveData does require a bit of hoop jumping.

好的,希望能帮助您继续前进.祝你好运!

Okay, hope that helps you move forward. Good luck!

代码链接

这篇关于如何防止 TypeScript 中的文字类型的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆