如何防止 TypeScript 中的文字类型 [英] How to prevent a literal type in 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 :(
最后一行失败,因为 b
是 Primitive<'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
),我能做些什么吗?
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.
我们可以使用条件类型强制(联合)字符串、数字和布尔文字扩展为(联合)string
、number
和 boolean
:
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
现在这很棒,您可能想要继续的一种方法是在 PrimitiveData
WidenLiteralsT
>:
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
就目前而言,这是有效的.bTest
是 PrimitiveDataTest<"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;
}
}
然后定义类型和值 PrimitiveData
与 Data
相关,如下所示:
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屋!