缩小相互依赖的、基于联合的泛型函数参数的类型



我有一个简单的函数,可以检查variable是否包含在数组中opts

type IType1 = 'text1' | 'text2';
type IType2 = 'text3' | 'text4' | 'text5';
const foo: IType2 = 'text4';
function oneOf(variable, opts) {
return opts.includes(variable);
}

我想要的是使optsvariable相互依赖,所以如果我调用该函数:

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的问题在于IType1IType2本身是字符串文本类型的联合。 编译器会将其折叠为仅T extends 'text1' | 'text2' | 'text3' | 'text4' | 'text5',因此编译器不会自然地根据IType1IType2对其进行分组。 如果传入类型为'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扩大到IType1IType2的效果。 请注意,函数的实现至少需要一个类型断言,因为编译器对未解析的泛型类型实际上无能为力......由于它不知道T是什么,所以它无法弄清楚T extends IType1 ? IType1 : IType2是什么;这种类型对编译器基本上是不透明的,因为计算是延迟的。 通过说optsreadonly (IType1 | IType2)[]我们只是说无论opts是什么,我们都能够从中读取IType1IType2类型的元素。


另一种方法是放弃泛型,只考虑不同的IType1IType2调用签名。 传统上,你会像这样使用重载来执行此操作:

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[]]

无论如何,所有这些版本都应该从调用端很好地工作,具体取决于您的用例。

操场链接到代码

最新更新