缩小相互依赖的、基于联合的、通用函数参数的类型
Narrowing down types of co-dependent, union-based, generic function arguments
我有一个简单的函数,它检查 variable
是否包含在数组 opts
中
type IType1 = 'text1' | 'text2';
type IType2 = 'text3' | 'text4' | 'text5';
const foo: IType2 = 'text4';
function oneOf(variable, opts) {
return opts.includes(variable);
}
我想要的是让 opts
和 variable
相互依赖,所以如果我调用函数:
oneOf(foo, ['text3', 'text5']) //=> I would get OK
oneOf(foo, ['text3', 'text2']) //=> I would get a warning here, because `IType2` (type of `foo`) does not contain 'text2'
方法一
如果我写:
function oneOf<T extends IType1 | IType2>(variable: T, opts: T[]): boolean{
return opts.includes(variable);
}
在这两种情况下我都会得到 OK
。 TS 会简单地假设在第二种情况下 T extends "text3" | "text4" | "text2"
,这不是我想要的。
方法二
如果我写
function oneOf<T1 extends IType1 | IType2, T2 extends T1>(variable: T1, opts: T2[]): boolean{
return opts.includes(variable);
}
我会得到一个错误:
Argument of type 'T1' is not assignable to parameter of type 'T2'.
'T1' is assignable to the constraint of type 'T2', but 'T2' could be instantiated with a
different subtype of constraint '"text1" | "text2" | "text3" | "text4" | "text5"'.
...
这完全可以在 TS 中完成吗?
constraint T extends IType1 | IType2
is that IType1
and IType2
are themselves unions of string literal types 的问题。编译器会将其压缩为 T extends 'text1' | 'text2' | 'text3' | 'text4' | 'text5'
,因此编译器不会自然地按照 IType1
和 IType2
对它们进行分组。如果您传入 'text4'
类型的值,那么编译器将推断 T
是 'text4'
而不是 IType2
.
所以有不同的方法来解决这个问题。一种是您可以将联合类型保留在 variable
类型的约束中,但对 opts
的元素类型使用 conditional type:
function oneOf<T extends IType1 | IType2>(
variable: T,
opts: Array<T extends IType1 ? IType1 : IType2>
): boolean {
return (opts as readonly (IType1 | IType2)[]).includes(variable);
}
oneOf('text4', ['text3', 'text5']) // okay
oneOf('text4', ['text3', 'text2']) // error
条件类型 T extends IType1 ? IType1 : IType2
的效果是 将 从 T
扩大到 IType1
或 IType2
。请注意,该函数的实现至少需要一个 type assertion,因为编译器实际上不能对未解析的泛型类型做太多事情......因为它不知道 T
是什么,所以它不能'不知道 T extends IType1 ? IType1 : IType2
是什么;这种类型对编译器来说本质上是不透明的,因为评估是延迟的。通过说 opts
是 readonly (IType1 | IType2)[]
我们只是说无论 opts
是什么,我们都能够从中读取 IType1
或 IType2
类型的元素。
另一种方法是放弃泛型,只考虑 IType1
和 IType2
的不同调用签名。传统上你会用 overloads 这样做:
function oneOf(variable: IType1, opts: IType1[]): boolean;
function oneOf(variable: IType2, opts: IType2[]): boolean;
function oneOf(variable: IType1 | IType2, opts: readonly (IType1 | IType2)[]) {
return opts.includes(variable);
}
oneOf('text4', ['text3', 'text5']); // okay
oneOf('text4', ['text3', 'text2']); // error
这很好,但是使用重载可能有点困难,而且它们的扩展性不是很好(如果你有 IType1 | IType2 | IType3 | ... | IType10
写出调用签名可能会很烦人)。
当每个调用签名的 return 类型相同(例如它们都是 boolean
)时,重载的替代方法是使用一个调用签名,该调用签名采用 rest parameter whose type is a union of tuples :
function oneOf(...[variable, opts]:
[variable: IType1, opts: IType1[]] |
[variable: IType2, opts: IType2[]]
): boolean {
return (opts as readonly (IType1 | IType2)[]).includes(variable);
}
oneOf('text4', ['text3', 'text5']); // okay
oneOf('text4', ['text3', 'text2']); // error
这实际上看起来很像调用方的过载。从那里你可以制作一个编程版本来为我们计算元组的并集:
type ValidArgs<T extends any[]> = {
[I in keyof T]: [variable: T[I], opts: T[I][]]
}[number];
function oneOf(...[variable, opts]: ValidArgs<[IType1, IType2]>): boolean {
return (opts as readonly (IType1 | IType2)[]).includes(variable);
}
oneOf('text4', ['text3', 'text5']); // okay
oneOf('text4', ['text3', 'text2']); // error
这和以前一样; ValidArgs<[IType1, IType2]>
的计算结果为 [variable: IType1, opts: IType1[]] | [variable: IType2, opts: IType2[]]
。它的工作原理是将输入元组类型 [IType1, IType2]
和 mapping over it to form a new type like [[variable: IType1, opts: IType1[]], [variable: IType2, opts: IType2[]]]
which we then immediately index into 与 number
索引一起获取该元组元素的并集;即 [variable: IType1, opts: IType1[]] | [variable: IType2, opts: IType2[]]
.
您可以看到 ValidArgs
如何更容易扩展:
type Test = ValidArgs<[0, 1, 2, 3, 4, 5]>;
// type Test = [variable: 0, opts: 0[]] | [variable: 1, opts: 1[]] | [variable: 2, opts: 2[]] |
// [variable: 3, opts: 3[]] | [variable: 4, opts: 4[]] | [variable: 5, opts: 5[]]
无论如何,根据您的用例,所有这些版本在调用方都应该工作得相当好。
我有一个简单的函数,它检查 variable
是否包含在数组 opts
type IType1 = 'text1' | 'text2';
type IType2 = 'text3' | 'text4' | 'text5';
const foo: IType2 = 'text4';
function oneOf(variable, opts) {
return opts.includes(variable);
}
我想要的是让 opts
和 variable
相互依赖,所以如果我调用函数:
oneOf(foo, ['text3', 'text5']) //=> I would get OK
oneOf(foo, ['text3', 'text2']) //=> I would get a warning here, because `IType2` (type of `foo`) does not contain 'text2'
方法一
如果我写:
function oneOf<T extends IType1 | IType2>(variable: T, opts: T[]): boolean{
return opts.includes(variable);
}
在这两种情况下我都会得到 OK
。 TS 会简单地假设在第二种情况下 T extends "text3" | "text4" | "text2"
,这不是我想要的。
方法二
如果我写
function oneOf<T1 extends IType1 | IType2, T2 extends T1>(variable: T1, opts: T2[]): boolean{
return opts.includes(variable);
}
我会得到一个错误:
Argument of type 'T1' is not assignable to parameter of type 'T2'.
'T1' is assignable to the constraint of type 'T2', but 'T2' could be instantiated with a
different subtype of constraint '"text1" | "text2" | "text3" | "text4" | "text5"'.
...
这完全可以在 TS 中完成吗?
constraint T extends IType1 | IType2
is that IType1
and IType2
are themselves unions of string literal types 的问题。编译器会将其压缩为 T extends 'text1' | 'text2' | 'text3' | 'text4' | 'text5'
,因此编译器不会自然地按照 IType1
和 IType2
对它们进行分组。如果您传入 'text4'
类型的值,那么编译器将推断 T
是 'text4'
而不是 IType2
.
所以有不同的方法来解决这个问题。一种是您可以将联合类型保留在 variable
类型的约束中,但对 opts
的元素类型使用 conditional type:
function oneOf<T extends IType1 | IType2>(
variable: T,
opts: Array<T extends IType1 ? IType1 : IType2>
): boolean {
return (opts as readonly (IType1 | IType2)[]).includes(variable);
}
oneOf('text4', ['text3', 'text5']) // okay
oneOf('text4', ['text3', 'text2']) // error
条件类型 T extends IType1 ? IType1 : IType2
的效果是 将 从 T
扩大到 IType1
或 IType2
。请注意,该函数的实现至少需要一个 type assertion,因为编译器实际上不能对未解析的泛型类型做太多事情......因为它不知道 T
是什么,所以它不能'不知道 T extends IType1 ? IType1 : IType2
是什么;这种类型对编译器来说本质上是不透明的,因为评估是延迟的。通过说 opts
是 readonly (IType1 | IType2)[]
我们只是说无论 opts
是什么,我们都能够从中读取 IType1
或 IType2
类型的元素。
另一种方法是放弃泛型,只考虑 IType1
和 IType2
的不同调用签名。传统上你会用 overloads 这样做:
function oneOf(variable: IType1, opts: IType1[]): boolean;
function oneOf(variable: IType2, opts: IType2[]): boolean;
function oneOf(variable: IType1 | IType2, opts: readonly (IType1 | IType2)[]) {
return opts.includes(variable);
}
oneOf('text4', ['text3', 'text5']); // okay
oneOf('text4', ['text3', 'text2']); // error
这很好,但是使用重载可能有点困难,而且它们的扩展性不是很好(如果你有 IType1 | IType2 | IType3 | ... | IType10
写出调用签名可能会很烦人)。
当每个调用签名的 return 类型相同(例如它们都是 boolean
)时,重载的替代方法是使用一个调用签名,该调用签名采用 rest parameter whose type is a union of tuples :
function oneOf(...[variable, opts]:
[variable: IType1, opts: IType1[]] |
[variable: IType2, opts: IType2[]]
): boolean {
return (opts as readonly (IType1 | IType2)[]).includes(variable);
}
oneOf('text4', ['text3', 'text5']); // okay
oneOf('text4', ['text3', 'text2']); // error
这实际上看起来很像调用方的过载。从那里你可以制作一个编程版本来为我们计算元组的并集:
type ValidArgs<T extends any[]> = {
[I in keyof T]: [variable: T[I], opts: T[I][]]
}[number];
function oneOf(...[variable, opts]: ValidArgs<[IType1, IType2]>): boolean {
return (opts as readonly (IType1 | IType2)[]).includes(variable);
}
oneOf('text4', ['text3', 'text5']); // okay
oneOf('text4', ['text3', 'text2']); // error
这和以前一样; ValidArgs<[IType1, IType2]>
的计算结果为 [variable: IType1, opts: IType1[]] | [variable: IType2, opts: IType2[]]
。它的工作原理是将输入元组类型 [IType1, IType2]
和 mapping over it to form a new type like [[variable: IType1, opts: IType1[]], [variable: IType2, opts: IType2[]]]
which we then immediately index into 与 number
索引一起获取该元组元素的并集;即 [variable: IType1, opts: IType1[]] | [variable: IType2, opts: IType2[]]
.
您可以看到 ValidArgs
如何更容易扩展:
type Test = ValidArgs<[0, 1, 2, 3, 4, 5]>;
// type Test = [variable: 0, opts: 0[]] | [variable: 1, opts: 1[]] | [variable: 2, opts: 2[]] |
// [variable: 3, opts: 3[]] | [variable: 4, opts: 4[]] | [variable: 5, opts: 5[]]
无论如何,根据您的用例,所有这些版本在调用方都应该工作得相当好。