TypeScript 有条件地映射元组可选元素

TypeScript conditionally map tuple optional elements

是否可以在 TypeScript 中创建映射类型以有条件地向元组类型元素添加可选修饰符?具体来说,我想映射一个元素可能未定义的元组类型,如果是这样,将该元素设置为可选。例如:

type Input = [string, number | undefined]

type UndefinedToOptional<T> = { [K in keyof T}: T[K] extends undefined ? ... : ... } // ???

type Output = UndefinedToOptional<Input> // should be [string, (number | undefined)?]

我可以创建一个映射类型,总是 添加可选修饰符:

type ToOptional<T> = { [K in keyof T]+?: T[K] }

type AllOptionalOutput = ToOptional<Input> // this is now [string?, (number | undefined)?]

但我不确定如何使可选修饰符成为条件。对于在对象上运行的映射类型,我将通过创建两个对象类型并将它们相交来实现这一点,其中所有属性都设置为可选,然后与一个对象相交,该对象挑选出所需的道具,但我不确定如何完成类似的事情元组。

请注意,您想要的定义可能存在问题。 Optional elements in tuple types 只允许在后面的每个元素也是可选的元素上。所以你可以写 [1, 2?, 3?] 而不是 [1?, 2?, 3]。这意味着如果你有一个像 [1, 2|undefined, 3, 4|undefined, 5|undefined] 这样的元组,你可以将它变成 [1, 2|undefined, 3, 4?, 5?][1, 2?, 3?, 4?, 5?] 而不是 [1, 2?, 3, 4?, 5?]。我假设前者(需要 3)是您想要的,如下所示。


不幸的是,要做到这一点并不容易。 TypeScript 中的元组类型操作有些初级。有一个悬而未决的问题,microsoft/TypeScript#26223 that asks for part of this, with my comment here 概括为回答此问题所需的任意元组操作类型。具体来说,根据该评论,需要 TupleLenOptional 之类的内容。

可以利用 TypeScript 提供给我们的部分构建一个实现,但也有缺点。明显的缺点是它丑陋且复杂;我们必须使用 prepend-to-tuple 和 split-tuple-into-first-and-rest 操作。与理想版本相比,这样的实现对编译器的负担更大,在理想版本中您大概只使用映射的元组。

如果 TypeScript 支持循环条件类型(请参阅 microsoft/TypeScript#26980 了解功能请求),您会希望使用它们。因为它没有,你要么必须欺骗编译器允许它们,这是非常不支持的......或者你必须将循环类型展开到一个冗余列表中,这些列表只适用于某些固定元组长度。

这是我对后者的实现,它应该适用于长度为 10 左右的元组:

type Cons<H, T extends any[]> = ((h: H, ...t: T) => void) extends ((...r: infer R) => void) ? R : never;
type Tail<T extends any[]> = ((...t: T) => void) extends ((h: any, ...r: infer R) => void) ? R : never;
type CondPartTuple<T extends any[]> = Extract<unknown extends { [K in keyof T]: undefined extends T[K] ? never : unknown }[number] ? T : Partial<T>, any[]>
type UndefinedToOptionalTuple<T extends any[]> = CondPartTuple<T['length'] extends 0 ? [] : Cons<T[0], PT0<Tail<T>>>>
type PT0<T extends any[]> = CondPartTuple<T['length'] extends 0 ? [] : Cons<T[0], PT1<Tail<T>>>>
type PT1<T extends any[]> = CondPartTuple<T['length'] extends 0 ? [] : Cons<T[0], PT2<Tail<T>>>>
type PT2<T extends any[]> = CondPartTuple<T['length'] extends 0 ? [] : Cons<T[0], PT3<Tail<T>>>>
type PT3<T extends any[]> = CondPartTuple<T['length'] extends 0 ? [] : Cons<T[0], PT4<Tail<T>>>>
type PT4<T extends any[]> = CondPartTuple<T['length'] extends 0 ? [] : Cons<T[0], PT5<Tail<T>>>>
type PT5<T extends any[]> = CondPartTuple<T['length'] extends 0 ? [] : Cons<T[0], PT6<Tail<T>>>>
type PT6<T extends any[]> = CondPartTuple<T['length'] extends 0 ? [] : Cons<T[0], PT7<Tail<T>>>>
type PT7<T extends any[]> = CondPartTuple<T['length'] extends 0 ? [] : Cons<T[0], PT8<Tail<T>>>>
type PT8<T extends any[]> = CondPartTuple<T['length'] extends 0 ? [] : Cons<T[0], PT9<Tail<T>>>>
type PT9<T extends any[]> = CondPartTuple<T['length'] extends 0 ? [] : Cons<T[0], PTX<Tail<T>>>>
type PTX<T extends any[]> = CondPartTuple<T>; // bail out

那里的基本思想:对元组类型进行右折叠(如 reduceRight())。在每一步中,您都有元组的头部(第一个元素)和尾部(其余元素)。将头部添加到尾部,然后检查 undefined 是否可分配给结果的每个元素。如果是,则将其更改为 Partial。否则别管它。

这达到了预期的效果:

type Result = UndefinedToOptionalTuple<[1, 2 | undefined, 3, 4 | undefined, 5 | undefined]>
// type Result = [1, 2 | undefined, 3, (4 | undefined)?, (5 | undefined)?]

但是……糟糕。我当然不会尝试在任何生产代码库中使用上面的代码,因为它会使编译器陷入困境并且看起来一团糟。所以,随心所欲吧。


好的,希望对您有所帮助;祝你好运!

Playground link to code