定义一个不允许并集字符串的 "extends string" 类型
Define an "extends string" type that doesn't allow union of strings
我有这个功能
function something<T extends string>(): void {
// ...
}
我想将 T
限制为单个字符串,而不是字符串的并集:
something<'stringOne'>(); // I want this to be allowed
something<'stringOne' | 'stringTwo'>(); // I DON'T want this to be allowed
有办法吗?
这是一种处理方法:
type NotAUnion<T> = [T] extends [infer U] ? _NotAUnion<U, U> : never;
type _NotAUnion<T, U> = U extends any ? [T] extends [U] ? unknown : never : never;
function something<T extends string & NotAUnion<T>>(): void {
// ...
}
something<'stringOne'>(); // okay
something<'stringOne' | 'stringTwo'>(); // error
您可以验证它是否有效。
NotAUnion<T>
的思路是,如果T
是一个union type, NotAUnion<T>
should resolve to the never
type (TypeScript's bottom type; nothing other than never
itself is assignable to never
); otherwise, if T
is not a union type, NotAunion<T>
should resolve to the unknown
type (TypeScript's top type;每种类型都可以分配给 unknown
).
constraint T extends string & NotAUnion<T>
is a recursive constraint, also known as F-bounded quantification,要让编译器认为这是可以接受的,这是一件棘手的事情。如果您不小心,您可能会收到有关循环类型定义的警告。我 会 以另一种方式解决这个问题,但是您的示例没有任何其他使用 T
的地方(通常是 doSomething<T>(t: T): void
其中 T
显示为参数类型或其他东西),所以我保留了一个版本,其中 T extends XXX
是表达约束的唯一方式。
无论如何,T extends string & NotAUnion<T>
表达了 T
必须可分配给 string
而不能是一个联合的想法:如果 T
是一个联合,那么 string & NotAUnion<T>
将解析为 string & never
,后者解析为 never
,因此将违反 T extends never
。另一方面,如果 T
不是联合,则 string & NotAUnion<T>
将解析为 string & unknown
,后者解析为 string
,因此 T extends string
将得到满足如果 T
可分配给 string
.
这为您提供了 "stringOne"
与 "stringOne" | "stringTwo"
所需的行为。它还将接受 string
本身,这不是联合。如果您想禁止这样做,您可以使用更复杂的约束来这样做,但我认为这超出了所问问题的范围(因为它不是指定的用例之一)。
所以这里唯一需要解释的就是NotAUnion<T>
是如何工作的。一般的想法是 NotAUnion<T>
将检查 T
是否可以单独分配给 T
的所有联合成员。如果是这样,那么 T
就不是联合(因为它只有一个成员)。如果不是,则 T
是联合。
这里的做法是先用conditional type inference with infer
to copy T
into another type parameter U
. By wrapping this in a single-element tuple, we are preventing this from being a distributive conditional type。所以在 [T] extends [infer U] ? ...T,U... : never
中,T
和 U
都是相同的类型。
所以 [T] extends [infer U] ? _NotAUnion<U, U> : never
本质上与 _NotAUnion<T, T>
相同,除了前者会使编译器抱怨循环。所以 T extends _NotAUnion<T, T>
被视为非法循环,但 T extends NotAUnion<T>
不是。从 T
复制到 U
然后检查 U
似乎是这里的重要部分。
无论如何,然后 _NotAUnion<T, U>
类型有意 分布 跨联合成员的类型函数 U
,通过编写分配条件类型 U extends any ? ..,T,U... : never
.在该类型函数中,T
将是原始的 T
类型,而 U
将是 T
的每个独立联合成员。因此 T
是 "stringOne" | "stringTwo"
,那么 U
将依次是 "stringOne"
和 "stringTwo"
。
最后,真正的比较:[T] extends [U] ? unknown : never
。这是另一种 non-distributive 类型(我们不想将 T
分成几块)。如果 T
不是并集,那么最终会变成类似于 "stringOne" extends "stringOne"
的结果,这将导致 unknown
。但是如果 T
是一个联合,那么这将变成 ("stringOne" | "stringTwo") extends "stringOne"
,这是错误的,所以你得到 never
。它还会检查 ("stringOne" | "stringTwo") extends "stringTwo"
,这与 false 一样。
好了。显然,这有点令人费解。还有其他方法可以编写这样的函数,但它们同样或更奇怪。 microsoft/TypeScript#34504 中提到了 NotAUnion<T>
的稍微不那么复杂的 one-liner 版本,但它似乎依赖于随 TypeScript 版本而变化的未指定行为。我并没有希望 one-liner 版本总是像我在这里展示的 two-line 版本那样工作,而是在这里使用更可靠的 two-line 版本。
我有这个功能
function something<T extends string>(): void {
// ...
}
我想将 T
限制为单个字符串,而不是字符串的并集:
something<'stringOne'>(); // I want this to be allowed
something<'stringOne' | 'stringTwo'>(); // I DON'T want this to be allowed
有办法吗?
这是一种处理方法:
type NotAUnion<T> = [T] extends [infer U] ? _NotAUnion<U, U> : never;
type _NotAUnion<T, U> = U extends any ? [T] extends [U] ? unknown : never : never;
function something<T extends string & NotAUnion<T>>(): void {
// ...
}
something<'stringOne'>(); // okay
something<'stringOne' | 'stringTwo'>(); // error
您可以验证它是否有效。
NotAUnion<T>
的思路是,如果T
是一个union type, NotAUnion<T>
should resolve to the never
type (TypeScript's bottom type; nothing other than never
itself is assignable to never
); otherwise, if T
is not a union type, NotAunion<T>
should resolve to the unknown
type (TypeScript's top type;每种类型都可以分配给 unknown
).
constraint T extends string & NotAUnion<T>
is a recursive constraint, also known as F-bounded quantification,要让编译器认为这是可以接受的,这是一件棘手的事情。如果您不小心,您可能会收到有关循环类型定义的警告。我 会 以另一种方式解决这个问题,但是您的示例没有任何其他使用 T
的地方(通常是 doSomething<T>(t: T): void
其中 T
显示为参数类型或其他东西),所以我保留了一个版本,其中 T extends XXX
是表达约束的唯一方式。
无论如何,T extends string & NotAUnion<T>
表达了 T
必须可分配给 string
而不能是一个联合的想法:如果 T
是一个联合,那么 string & NotAUnion<T>
将解析为 string & never
,后者解析为 never
,因此将违反 T extends never
。另一方面,如果 T
不是联合,则 string & NotAUnion<T>
将解析为 string & unknown
,后者解析为 string
,因此 T extends string
将得到满足如果 T
可分配给 string
.
这为您提供了 "stringOne"
与 "stringOne" | "stringTwo"
所需的行为。它还将接受 string
本身,这不是联合。如果您想禁止这样做,您可以使用更复杂的约束来这样做,但我认为这超出了所问问题的范围(因为它不是指定的用例之一)。
所以这里唯一需要解释的就是NotAUnion<T>
是如何工作的。一般的想法是 NotAUnion<T>
将检查 T
是否可以单独分配给 T
的所有联合成员。如果是这样,那么 T
就不是联合(因为它只有一个成员)。如果不是,则 T
是联合。
这里的做法是先用conditional type inference with infer
to copy T
into another type parameter U
. By wrapping this in a single-element tuple, we are preventing this from being a distributive conditional type。所以在 [T] extends [infer U] ? ...T,U... : never
中,T
和 U
都是相同的类型。
所以 [T] extends [infer U] ? _NotAUnion<U, U> : never
本质上与 _NotAUnion<T, T>
相同,除了前者会使编译器抱怨循环。所以 T extends _NotAUnion<T, T>
被视为非法循环,但 T extends NotAUnion<T>
不是。从 T
复制到 U
然后检查 U
似乎是这里的重要部分。
无论如何,然后 _NotAUnion<T, U>
类型有意 分布 跨联合成员的类型函数 U
,通过编写分配条件类型 U extends any ? ..,T,U... : never
.在该类型函数中,T
将是原始的 T
类型,而 U
将是 T
的每个独立联合成员。因此 T
是 "stringOne" | "stringTwo"
,那么 U
将依次是 "stringOne"
和 "stringTwo"
。
最后,真正的比较:[T] extends [U] ? unknown : never
。这是另一种 non-distributive 类型(我们不想将 T
分成几块)。如果 T
不是并集,那么最终会变成类似于 "stringOne" extends "stringOne"
的结果,这将导致 unknown
。但是如果 T
是一个联合,那么这将变成 ("stringOne" | "stringTwo") extends "stringOne"
,这是错误的,所以你得到 never
。它还会检查 ("stringOne" | "stringTwo") extends "stringTwo"
,这与 false 一样。
好了。显然,这有点令人费解。还有其他方法可以编写这样的函数,但它们同样或更奇怪。 microsoft/TypeScript#34504 中提到了 NotAUnion<T>
的稍微不那么复杂的 one-liner 版本,但它似乎依赖于随 TypeScript 版本而变化的未指定行为。我并没有希望 one-liner 版本总是像我在这里展示的 two-line 版本那样工作,而是在这里使用更可靠的 two-line 版本。