拨开荷叶行,寻梦已然成。仙女莲花里,翩翩白鹭情。
IMG-LOGO
主页 文章列表 缩小相互依赖的、基于联合的、通用函式自变量的型别

缩小相互依赖的、基于联合的、通用函式自变量的型别

白鹭 - 2022-02-17 2124 0 0

我有一个简单的函式,它检查是否variable包含在阵列中opts

type IType1 = 'text1' | 'text2';
type IType2 = 'text3' | 'text4' | 'text5';

const foo: IType2 = 'text4';

function oneOf(variable, opts) {
    return opts.includes(variable);
}

我想要的是使optsandvariable相互依赖,所以如果我呼叫该函式:

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);
}

在这两种情况下我都会得到OKTS 只会假设在第二种情况下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中完成吗?

uj5u.com热心网友回复:

约束 的问题T extends IType1 | IType2在于IType1IType2它们本身就是字符串文字型别的联合编译器会将其折叠为 just ,因此编译器不会自然地根据and对它们进行分组如果你传入一个型别的值,那么编译器会推断出is和 not T extends 'text1' | 'text2' | 'text3' | 'text4' | 'text5'IType1IType2'text4'T'text4'IType2

所以有不同的方法来解决这个问题。一种是您可以将该联合型别保留在 of 型别的约束中,但对 of 元素的型别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具有扩大TIType1或的效果IType2请注意,该函式的实作至少需要一个型别断言,因为编译器实际上无法对未决议的泛型型别做太多事情……因为它不知道是什么T,所以它无法弄清楚是什么T extends IType1 ? IType1 : IType2这种型别对编译器本质上是不透明的,因为评估被推迟了。通过说optsreadonly (IType1 | IType2)[]我们只是说无论opts是什么,我们都能够读取型别的元素IType1IType2从中读取元素。


解决此问题的另一种方法是放弃泛型,而只考虑 and 的不同呼叫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[]]

无论如何,根据您的用例,所有这些版本都应该在呼叫端运行得相当好。

Playground 代码链接

标签:

0 评论

发表评论

您的电子邮件地址不会被公开。 必填的字段已做标记 *