arrays - 可以在 Typescript 中定义重复的元组吗

标签 arrays typescript recursion tuples typescript-typings

我最近遇到了一个 API,其中的数组由任意数量的重复元组构成。我尝试了一些替代方法来实现它,但遇到了具有无限递归的类型或只是不表达约束的问题。
我如何能够按照下面的情况输入 RepeatedTuple?

//Possibly a recursive conditional way to define RepeatedTuple?
// https://github.com/microsoft/TypeScript/pull/40002

type Example = RepeatedTuple<[string,number]>;

const example0:Example = [];
const example1:Example = ["hello",1];
const example2:Example = ["hello",1,"hello",2];
const example3:Example = ["hello",1,"hello",2,"hello",3];
const example4:Example = ["hello",1,"hello",2,"hello",3,"hello",4];
https://www.typescriptlang.org/play?ts=4.2.0-beta&ssl=1&ssc=1&pln=10&pc=68#code/PTAKHsGdISwIwDYE8AEBDFAnApgYwK6awBu2Ku4AdgCYwAuMVaCKA7mqneCtdgGYxKZAErYADtjR1s1ACr4xCbAH4AUCBQALOnTGQAXCADm9TfjgA6CgFtg1mLkxRwfOsFlIJAZUcwxbsXwEBGAAFgAGSIAmVVU6TzIAUQAPNGtFMgBeFFEJKRl5DIAeAG1IOkxBIwAaSnxrOGxMAF0APgBuWIpKcpRsVPSlcP0UtIyUbJLmzu7e-rGlAEYRgfHJgCJNbGDwderF6dVZuj7VpSiVhayUEs3thF396rudvajD49OrgGZLwevbltXk8Xg83s8gWDqt8PlQ5mdsKE-msbqDHosIfdHlFMcDvrioaFpkA 有一个 Typescript 游乐场有上述情况。

最佳答案

TypeScript 中没有任何特定类型表示“由 string, number 的任意数量的副本组成的 tuple”。在这种情况下,你能得到的最接近的是一些有限的 union涵盖您实际预期的用例,或一些自我引用 generic可用于验证候选元组的约束。
请注意,虽然后者似乎允许完全任意长度的元组,但编译器对条件类型的递归限制相当浅(深度约为 25 左右)。因为有限联合与元组的长度成线性比例,所以实际上与递归相比,它会走得更远。

这是一种写法 RepeatedTuple对于长达 40 次重复的长度:

type _R<T extends readonly any[], U extends readonly any[]> = 
  readonly [] | readonly [...U, ...T];
type RepeatedTuple<T extends readonly any[]> =
    _R<T, _R<T, _R<T, _R<T, _R<T, _R<T, _R<T, _R<T, _R<T, _R<T, _R<T, _R<T, _R<T,
        _R<T, _R<T, _R<T, _R<T, _R<T, _R<T, _R<T, _R<T, _R<T, _R<T, _R<T, _R<T,
            _R<T, _R<T, _R<T, _R<T, _R<T, _R<T, _R<T, _R<T, _R<T, _R<T, _R<T,
                _R<T, _R<T, _R<T, _R<T, readonly []>>>>>
            >>>>>>>>>>>>>>>>>>>
        >>>>>>>>>>>>>
    >>>;
这使用 variadic tuples连接数组,我们只需手动组合一个联合或连接操作 40 次。如果需要,您可以添加到其中,尽管最终这会达到一定的限制。让我们看看它是如何工作的:
type Example = RepeatedTuple<[string, number]>
const example0: Example = [];
const example1: Example = ["hello", 1];
const example2: Example = ["hello", 1, "hello", 2];
const example3: Example = ["hello", 1, "hello", 2, "hello", 3];
const example4: Example = ["hello", 1, "hello", 2, "hello", 3, "hello", 4];

const badExample0: Example = ["hello"] // error!
// Source has 1 element(s) but target requires 80.
const badExample1: Example = ["hello", "goodbye"]; // error!
// Type 'string' is not assignable to type 'number | undefined'.
const badExample2: Example = ["hello", 1, "hello"]; // error!
// Source has 3 element(s) but target requires 80.
const badExample3: Example = ["hello", 1, "hello", 2, false]; // error!
// Type 'false' is not assignable to type 'string | undefined'.
const badExample4: Example = ["hello", 1, "hello", 2, "hello"]; // error!
// Source has 5 element(s) but target requires 80.


const limits: Example = ["", 0, "", 0, "", 0, "", 0, "", 0, "", 0, "", 0,
    "", 0, "", 0, "", 0, "", 0, "", 0, "", 0, "", 0, "", 0, "", 0, "", 0,
    "", 0, "", 0, "", 0, "", 0, "", 0, "", 0]; // okay
这看起来不错。连比较长的limits示例工作。失败案例的一些错误消息有点奇怪,因为编译器在解释你犯错的原因时侧重于长度为 80 的元组。但它按预期工作。但是,如果您创建了一个长度为 82 或更大的元组,这将失败。

为了完整起见,让我们探索通用约束方法:
type ValidRepeatedTuple<T extends readonly any[], U extends readonly any[]> =
    U extends readonly [] ? U :
    readonly [...T, ...(U extends readonly [...T, ...infer R] ? ValidRepeatedTuple<T, R> : [])];

const asValidRepeatedTuple = <T extends readonly any[]>() =>
    <U extends readonly any[]>(u: ValidRepeatedTuple<T, U>) => u;
这是使用 recursive conditional type检查候选元组 U反对元组到重复的串联 T .如 UT 的有效重复,然后 U将延长 ValidRepeatedTuple<T, U> .否则,ValidRepeatedTuple<T, U>将一个“附近”有效元组,以便错误消息将给出关于如何修复错误的更合理的提示。
请注意,添加泛型意味着需要指定泛型类型参数。为了让开发人员不必完全手动执行此操作,我添加了帮助函数 asValidRepeatedTuple()以便 U可以推断。此外,由于您想手动指定 T ,我们需要制作 asValidRepeatedTuple() curried函数,因为目前无法进行 microsoft/TypeScript#26242 中请求的排序的部分类型参数推断;在具有类型参数的单个泛型函数调用中 TU ,您要么需要手动指定两者,要么让编译器推断两者。柯里化允许您手动指定 T同时让编译器推断 U , 代价是在那里有一个额外的函数调用。
所以,这有点复杂……它有效吗?让我们来看看:
const asExample = asValidRepeatedTuple<[string, number]>();

const example0 = asExample([]);
const example1 = asExample(["hello", 1]);
const example2 = asExample(["hello", 1, "hello", 2]);
const example3 = asExample(["hello", 1, "hello", 2, "hello", 3]);
const example4 = asExample(["hello", 1, "hello", 2, "hello", 3, "hello", 4]);

const badExample0 = asExample(["hello"]); // error!
// Source has 1 element(s) but target requires 2.
const badExample1 = asExample(["hello", "goodbye"]); // error!
// Type 'string' is not assignable to type 'number'.
const badExample2 = asExample(["hello", 1, "hello"]); // error!
// Source has 3 element(s) but target requires 4.
const badExample3 = asExample(["hello", 1, "hello", 2, false]); // error!
// Type 'boolean' is not assignable to type 'string'.
const badExample4 = asExample(["hello", 1, "hello", 2, "hello"]); // error!
// Source has 5 element(s) but target requires 6.
是的,看起来不错。错误消息比以前的版本更好,因为它们提到了一个比编译器从有限联合中选择的更“附近”的元组。 (与其说“你的长度 3 元组不好,你应该让它成为 80”,而是说“你应该让它成为 4。)
但这里最大的缺点是递归限制:
const limits = asExample(["", 0, "", 0, "", 0, "", 0, "", 0, "", 0, "", 0,
    "", 0, "", 0, "", 0, "", 0, "", 0, "", 0, "", 0, "", 0, "", 0, "", 0,
    "", 0, "", 0, "", 0, "", 0, "", 0, "", 0]); // error!
// Type instantiation is excessively deep and possibly infinite.(2589)
长度为 46 的元组太长,因为编译器递归太多。有限联合看起来更有限,但实际上更擅长处理更长的元组。您可以通过手动展开一些递归来解决这个问题,从而将限制扩大到更大一些。但它不太可能达到手动联合。
您可能能够编写一个不太直接的递归通用约束,通过在递归的每一步将元组加倍或不加倍,该约束随元组的长度成对数缩放。看 this GitHub issue comment对于类似的技术。这可能适用于很长的元组,但有人很难理解它在做什么(甚至比这里已经存在的东西更难)而且我还没有让它工作......所以我放弃了。
最后,即使您完善了这一点,也要注意它只能真正用于验证候选特定元组。编译器将无法遵循通用元组的逻辑。因此,例如,在接受 ValidRepeatedTuple<T, U> 的函数中对于一些通用的 U ,编译器将无可救药地迷失于这意味着什么:
function hmm<U extends readonly any[]>(t: ValidRepeatedTuple<[string, number], U>) {
    t[0].toUpperCase() // compiler thinks it's a string, but hey, it might be undefined
    t[1].toFixed() // compiler thinks it's a number, but hey, it might be undefined
    t[2]?.toUpperCase() // error! compiler thinks it's undefined, but hey, it might be a string
}
对于 big-old-union 案例,情况稍微好一些:
function hmm(t: Example) {
    t[0]?.toUpperCase() // okay
    t[1]?.toFixed() // okay
    t[2]?.toUpperCase() // okay
    for (let i = 0; i < t.length / 2; i++) {
        t[2 * i]?.toUpperCase(); // error
        t[2 * i + 1]?.toFixed(); // error
    }
}
虽然你可以看到它并不真正理解你可能正在做的操作类型,所以🤷‍♂️。

Playground link to code

关于arrays - 可以在 Typescript 中定义重复的元组吗,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/66298024/

相关文章:

javascript - jQuery - 查找和删除 div 的多个属性

arrays - 无法对数组进行切片

typescript - 如何从字符串输入中使用 RxJs 5 的主题和去抖动

typescript - 防止 typescript 允许未定义的返回

python - python中递归函数的累积结果

javascript - JavaScript 中的递归和 setTimeout

c - 定义未定义数组并从 c 中的结构返回时出现段错误

javascript - TweenJS 不尊重实例?

reactjs - 如何在 vscode webview 中使用acquireVsCodeApi [React]

algorithm - 我应该对算法使用递归还是内存?