我有一个简单的函数,可以检查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'
方法1
如果我写:
function oneOf<T extends IType1 | IType2>(variable: T, opts: T[]): boolean{
return opts.includes(variable);
}
在这两种情况下,我都会得到OK
。TS只是假设在第二种情况下T extends "text3" | "text4" | "text2"
,这不是我想要的。
方法2
如果我写
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 中完全可以完成吗?
约束T extends IType1 | IType2
的问题在于IType1
和IType2
本身是字符串文本类型的联合。 编译器会将其折叠为仅T extends 'text1' | 'text2' | 'text3' | 'text4' | 'text5'
,因此编译器不会自然地根据IType1
和IType2
对其进行分组。 如果传入类型为'text4'
的值,则编译器将推断T
'text4'
而不是IType2
。
因此,有不同的方法可以解决这个问题。 一种是您可以将该联合类型保留在variable
类型的约束中,但对opts
元素的类型使用条件类型:
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
的效果。 请注意,函数的实现至少需要一个类型断言,因为编译器对未解析的泛型类型实际上无能为力......由于它不知道T
是什么,所以它无法弄清楚T extends IType1 ? IType1 : IType2
是什么;这种类型对编译器基本上是不透明的,因为计算是延迟的。 通过说opts
是readonly (IType1 | IType2)[]
我们只是说无论opts
是什么,我们都能够从中读取IType1
或IType2
类型的元素。
另一种方法是放弃泛型,只考虑不同的IType1
和IType2
调用签名。 传统上,你会像这样使用重载来执行此操作:
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
写出呼叫签名可能会很烦人)。
当每个调用签名的返回类型相同(就像它们都是boolean
)时,重载的替代方法是有一个调用签名,该签名采用一个 rest 参数,其类型是元组的联合:
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]
并映射到它上面以形成一个新类型,例如[[variable: IType1, opts: IType1[]], [variable: IType2, opts: IType2[]]]
然后我们立即与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[]]
无论如何,所有这些版本都应该从调用端很好地工作,具体取决于您的用例。
操场链接到代码