TypeScript 按任意类型过滤元组类型

Posted

技术标签:

【中文标题】TypeScript 按任意类型过滤元组类型【英文标题】:TypeScript filter tuple type by an arbitrary type 【发布时间】:2020-03-17 22:20:44 【问题描述】:

如何通过提供的元组中的任意类型过滤提供的元组类型来生成新的元组类型?

示例 (Playground):

type Journey = ["don't", 'stop', 'believing'];

type ExcludeFromTuple<T extends unknown[], E> = ????;

type DepressingJourney = ExcludeFromTuple<Journey, "don't">; // type should be ['stop', 'believing']

请注意,该解决方案不需要事先确保 E 类型存在于类型 T 中,如果存在则只需将其删除即可。

虽然这里的示例很简单,但我有一个更复杂的用例,我希望能够通过我正在编写的库的使用者定义的任意类型进行过滤。

虽然 TypeScript 原生支持 exclude type,但它只适用于联合类型,我一直无法找到元组的等价物。

ExcludeFromTuple 这样的类型对于生成其他实用程序类型非常有用。

type RemoveStringsFromTuple<T extends unknown[]> = ExcludeFromTuple<T, string>;
type RemoveNumbersFromTuple<T extends unknown[]> = ExcludeFromTuple<T, number>;
type RemoveNeversFromTuple<T extends unknown[]> = ExcludeFromTuple<T, never>;
type RemoveUndefinedsFromTuple<T extends unknown[]> = ExcludeFromTuple<T, undefined>;

我感觉该类型需要利用 TypeScript 2.8 的 conditional types 的组合, TypeScript 3.1 的 mapped types on tuples,以及某种类型的递归类型魔法,但我无法弄清楚,也找不到任何人。

【问题讨论】:

【参考方案1】:

TS 4.1+ 的更新:

variadic tuple types 在 TS 4.0 中引入,recursive conditional types 在 TS4.1 中引入,您现在可以将ExcludeFromTuple 更简单地写为:

type ExcludeFromTuple<T extends readonly any[], E> =
    T extends [infer F, ...infer R] ? [F] extends [E] ? ExcludeFromTuple<R, E> :
    [F, ...ExcludeFromTuple<R, E>] : []

您可以验证这是否按需要工作:

type DepressingJourney = ExcludeFromTuple<Journey, "don't">;
// type should be ['stop', 'believing']

type SlicedPi = ExcludeFromTuple<[3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9], 1 | 9>
// type SlicedPi = [3, 4, 5, 2, 6, 5, 3, 5, 8, 7]

Playground link to code


TS-4.1 之前的答案:

糟糕,这确实需要recursive conditional types,TypeScript 尚不支持。如果您想使用它们,请这样做at your own risk。通常我宁愿写一个应该是递归的类型,然后将它展开到一个固定的深度。所以我写的是type F&lt;X&gt; = ...F0&lt;X&gt;...; type F0&lt;X&gt; = ...F1&lt;X&gt;...;,而不是type F&lt;X&gt; = ...F&lt;X&gt;...

要写这篇文章,我想对元组使用基本的“列表处理”类型,即Cons&lt;H, T&gt; 将类型H 添加到元组T 上; Head&lt;T&gt; 获取元组T 的第一个元素,Tail&lt;T&gt; 获取删除第一个元素的元组T。你可以这样定义:

type Cons<H, T> = T extends readonly any[] ? ((h: H, ...t: T) => void) extends ((...r: infer R) => void) ? R : never : never;
type Tail<T extends readonly any[]> = ((...t: T) => void) extends ((h: any, ...r: infer R) => void) ? R : never;
type Head<T extends readonly any[]> = T[0];

然后递归类型看起来像这样:

/* type ExcludeFromTupleRecursive<T extends readonly any[], E> = 
     T["length"] extends 0 ? [] : 
     ExcludeFromTupleRecursive<Tail<T>, E> extends infer X ? 
     Head<T> extends E ? X : Cons<Head<T>, X> : never; */

想法是:取元组T 的尾部并对其执行ExcludeFromTupleRecursive。这就是递归。然后,对于结果,当且仅当它与 E 不匹配时,您才应该在元组的头部前面添加。

但这是非法循环,所以我这样展开:

type ExcludeFromTuple<T extends readonly any[], E> = T["length"] extends 0 ? [] : X0<Tail<T>, E> extends infer X ? Head<T> extends E ? X : Cons<Head<T>, X> : never;
type X0<T extends readonly any[], E> = T["length"] extends 0 ? [] : X1<Tail<T>, E> extends infer X ? Head<T> extends E ? X : Cons<Head<T>, X> : never;
type X1<T extends readonly any[], E> = T["length"] extends 0 ? [] : X2<Tail<T>, E> extends infer X ? Head<T> extends E ? X : Cons<Head<T>, X> : never;
type X2<T extends readonly any[], E> = T["length"] extends 0 ? [] : X3<Tail<T>, E> extends infer X ? Head<T> extends E ? X : Cons<Head<T>, X> : never;
type X3<T extends readonly any[], E> = T["length"] extends 0 ? [] : X4<Tail<T>, E> extends infer X ? Head<T> extends E ? X : Cons<Head<T>, X> : never;
type X4<T extends readonly any[], E> = T["length"] extends 0 ? [] : X5<Tail<T>, E> extends infer X ? Head<T> extends E ? X : Cons<Head<T>, X> : never;
type X5<T extends readonly any[], E> = T["length"] extends 0 ? [] : X6<Tail<T>, E> extends infer X ? Head<T> extends E ? X : Cons<Head<T>, X> : never;
type X6<T extends readonly any[], E> = T["length"] extends 0 ? [] : X7<Tail<T>, E> extends infer X ? Head<T> extends E ? X : Cons<Head<T>, X> : never;
type X7<T extends readonly any[], E> = T["length"] extends 0 ? [] : X8<Tail<T>, E> extends infer X ? Head<T> extends E ? X : Cons<Head<T>, X> : never;
type X8<T extends readonly any[], E> = T["length"] extends 0 ? [] : X9<Tail<T>, E> extends infer X ? Head<T> extends E ? X : Cons<Head<T>, X> : never;
type X9<T extends readonly any[], E> = T["length"] extends 0 ? [] : XA<Tail<T>, E> extends infer X ? Head<T> extends E ? X : Cons<Head<T>, X> : never;
type XA<T extends readonly any[], E> = T["length"] extends 0 ? [] : XB<Tail<T>, E> extends infer X ? Head<T> extends E ? X : Cons<Head<T>, X> : never;
type XB<T extends readonly any[], E> = T["length"] extends 0 ? [] : XC<Tail<T>, E> extends infer X ? Head<T> extends E ? X : Cons<Head<T>, X> : never;
type XC<T extends readonly any[], E> = T["length"] extends 0 ? [] : XD<Tail<T>, E> extends infer X ? Head<T> extends E ? X : Cons<Head<T>, X> : never;
type XD<T extends readonly any[], E> = T["length"] extends 0 ? [] : XE<Tail<T>, E> extends infer X ? Head<T> extends E ? X : Cons<Head<T>, X> : never;
type XE<T extends readonly any[], E> = T; // bail out

玩得开心吗?让我们看看它是否有效:

type DepressingJourney = ExcludeFromTuple<Journey, "don't">;
// type should be ['stop', 'believing']

type SlicedPi = ExcludeFromTuple<[3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9], 1 | 9>
// type SlicedPi = [3, 4, 5, 2, 6, 5, 3, 5, 8, 7]

我觉得不错。

Link to code

【讨论】:

哇,这很漂亮,正是我想要的。官方的递归条件类型支持会帮助很多,但展开的版本对我的用例来说很好用。非常感谢! 这可以和never一起工作吗? ExcludeFromTuple&lt;[string, never, number], never&gt; 给了我never 而不是[string, number]。事实上,无论我试图排除什么,如果元组包含一个 never,我会得到 never 而不是一个元组。 @snarf this 修复了吗?问题是这里的类型在联合类型中是分布的,never 被认为是空联合,会发生奇怪的事情。

以上是关于TypeScript 按任意类型过滤元组类型的主要内容,如果未能解决你的问题,请参考以下文章

第七节:在 TypeScript 中什么是类型推论?

如何在 TypeScript 3+ 中正确键入通用元组剩余参数?

typeScript中的数据类型

TypeScript类型

TypeScript系列教程06基础类型

从0开始的TypeScriptの三:类型系统 ·下