繁体   English   中英

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

[英]Can repeated tuples be defined in Typescript

我最近遇到了一个 API ,其中 arrays 由任意数量的重复元组构成。 我尝试了一些替代方法来实现它,但遇到了具有无限递归或只是不表达约束的类型的问题。

根据以下情况,我将如何键入 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];

There is a Typescript playground at https://www.typescriptlang.org/play?ts=4.2.0-beta&ssl=1&ssc=1&pln=10&pc=68#code/PTAKHsGdISwIwDYE8AEBDFAnApgYwK6awBu2Ku4AdgCYwAuMVaCKA7mqneCtdgGYxKZAErYADtjR1s1ACr4xCbAH4AUCBQALOnTGQAXCADm9TfjgA6CgFtg1mLkxRwfOsFlIJAZUcwxbsXwEBGAAFgAGSIAmVVU6TzIAUQAPNGtFMgBeFFEJKRl5DIAeAG1IOkxBIwAaSnxrOGxMAF0APgBuWIpKcpRsVPSlcP0UtIyUbJLmzu7e-rGlAEYRgfHJgCJNbGDwderF6dVZuj7VpSiVhayUEs3thF396rudvajD49OrgGZLwevbltXk8Xg83s8gWDqt8PlQ5mdsKE-msbqDHosIfdHlFMcDvrioaFpkA which has the above case.

TypeScript 中没有任何特定类型表示“由任意数量的string, number副本组成的元组,数字”。 在这种情况下,您可以获得的最接近的可能是一些涵盖您实际预期的用例的有限联合,或者是一些可用于验证候选元组的自引用通用约束。

请注意,虽然后者似乎允许完全任意长度的元组,但编译器对条件类型的递归限制相当浅(深度约为 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 []>>>>>
            >>>>>>>>>>>>>>>>>>>
        >>>>>>>>>>>>>
    >>>;

这使用可变元组来连接 arrays,我们只需手动组合一个联合或连接操作 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示例也有效。 失败案例的一些错误消息有点奇怪,因为编译器在解释你犯错的原因时专注于 tuple-of-length-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;

这是使用递归条件类型来检查候选元组U与 tuple-to-repeat T的串联。 如果UT的有效重复,则U将扩展ValidRepeatedTuple<T, U> 否则, ValidRepeatedTuple<T, U>将是一个“附近的”有效元组,因此错误消息将给出关于如何修复错误的更合理的提示。

注意,添加 generics 意味着需要指定泛型类型参数。 为了让开发人员不必完全手动执行此操作,我添加了帮助程序 function asValidRepeatedTuple()以便可以推断出U 此外,由于您手动指定T ,我们需要将asValidRepeatedTuple()设置为curried function 因为目前无法进行microsoft/TypeScript#26242中请求的排序的部分类型参数推断 在使用类型参数TU的单个通用 function 调用中,您需要手动指定两者,或者让编译器推断两者。 currying 允许您在让编译器推断U的同时手动指定T ,但代价是在那里有一个额外的 function 调用。

所以,这有点复杂......它有效吗? 让我们来看看:

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 的元组太长,因为编译器递归太多。 看起来更有限的有限联合实际上更擅长处理更长的元组。 您可以通过手动展开一些递归来解决此问题,从而将限制扩展到更大的范围。 但它不太可能达到手动工会的程度。

可能可以编写一个不太直接的递归通用约束,通过在递归的每个步骤中将元组加倍或不加倍来与元组的长度成对数缩放。 查看此 GitHub 问题评论以了解类似技术。 这可能适用于很长的元组,但有人很难理解它在做什么(甚至比已经在这里的东西更难)而且我还没有让它工作......所以我放弃了。

最后,即使您完善了这一点,请注意它只能真正用于验证候选特定元组。 编译器将无法遵循通用元组的逻辑。 因此,例如,在 function 中,它接受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 代码链接

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM