首页 > 解决方案 > 必需元素不能跟随可选元素

问题描述

我正在尝试创建一个type基本上是具有以下可能性的数组的打字稿:

这里的顺序很重要string必需的,但boolean可选的,并且必须是第一个参数(如果适用)。

但是当我将其声明为以下代码时:

type BooleanAndString = [ boolean?, string ];

它显示:必需元素不能跟随可选元素 (1257)

可以使用以下代码解决此问题:

type BooleanAndString = [ boolean, string ] |
                        [ string ];

但它使代码更复杂一些,一旦它是我需要的一个非常简化的案例。我想将它用作按顺序来自 JSON 数组的元组。

为了让我的问题更清楚,以下结构是我所拥有的复杂结构之一,但我遇到了同样的问题:

type Message = [
    visible?: boolean,
    relatedField?: string,
    level: 's' | 'w' | 'i' | 'e' | 'd' | 'c',
    displayLevelDefaultTitle?: boolean,
    message: string,
    mergeable?: boolean
];

标签: typescript

解决方案


序言:我真的建议重构,以便您的代码TypeScript 一起工作,而不是与之对抗。这意味着你所有的可选参数都应该移到最后,如果你想跳过一个可选参数并传递一个以后的参数,你需要显式地发送undefined. 意思是,如果你有(p2: 2, p1?: 1, p3?: 3)=>void,你调用f(2), f(2,1), f(2,1,3), 或f(2,undefined,3).

请注意,这样做可以使您的函数实现更加健全,因为每个参数都将位于已知索引处,您只需检查该索引的值即可undefined。没有这个,你会发现要弄清楚你的参数实际在哪里是相当复杂的。(有时甚至是不可能的,因为参数的一些排序/类型是模棱两可的,比如[surname?: string, occupation?: string]哪里["Baker"]可以指代名为 Jane Baker 的裁缝或名为 Jane Tailor 的面包师。)正是这种健全的实现推动了 TypeScript 关于可选参数允许位置的规则。

或者,更好的是,您可以只创建对象类型的单个参数,而不必担心参数顺序。忘记带标签的元组,使用实际的对象键。所以你会(arg: {p1?: 1, p2: 2, p3?: 3})=>void打电话给f({p2: 2}), f({p2: 2, p1: 1}), f({p2: 2, p1: 1, p3: 3}), 或f({p2: 2, p3: 3}). 是的,它有点冗长,但它不需要调用者必须记住一些坦率的任意参数顺序。这对于实现来说更容易使用,因为您只需使用命名键而不是编号索引。

好的,从这里开始,让我们假设您无法重构,并且您有自己的方法来解析函数参数以将正确的值拉出正确的位置......这意味着这些问题超出了本文的范围问题。向前:


您的解决方法

type BooleanAndString = [ boolean, string ] | [ string ];

可能是唯一合理的前进方式。如果您的问题是您不想手动Message执行此操作,则可以使用递归条件类型以及可变元组类型和少量标记的元组元素来以编程方式实现这一点。

首先,由于您不能自己使用?可选标记,因此我们为其定义一个替代标记:

type OptionalMarker<T> = { __optional: T };

然后,我们将采用您的Message定义并将其更改为使用OptionalMarker而不是?. 我们将把它重命名为别的东西,这样它就Message可以成为我们转换的输出:

type MessageMarker = [
    visible: OptionalMarker<boolean>,
    relatedField: OptionalMarker<string>,
    level: Level,
    displayLevelDefaultTitle: OptionalMarker<boolean>,
    message: string,
    mergeable: OptionalMarker<boolean>
];

type Level = 's' | 'w' | 'i' | 'e' | 'd' | 'c'

哦,我已经创建了Level类型别名,这样 IntelliSense 就不会一直重复's' | 'w' | ......以后到处都是。

现在我们将定义Optionalize<T>,它将类似于MessageMarker元组的联合,其中可选元素在预期的位置存在或不存在。

type Optionalize<T extends any[]> = T extends [infer F, ...infer R] ?
    [...[F] extends [OptionalMarker<infer U>] ?
        LabeledSingleton<T, U> | [] : LabeledSingleton<T>, ...Optionalize<R>] : []

type LabeledSingleton<T extends any[], V = T[0]> = T extends [x: any, ...args: infer R] ?
    T extends [...infer L, ...R] ? { [K in keyof L]: V } : never : never;

在这里,Optionalize<T>检查 的每个元素T是否OptionalMarker<U>为 some U。如果是这样,则包含或排除U。如果没有,则包含该元素而不更改它。

业务 withLabeledSingleton<T, V>是一种获取带有标签的元组类型的方法 like[foo: 1, bar: 2, baz: 3]并返回一个单元素元组,其标签与 的第一个标签相同T,其值类型为VLabeledSingleton<[foo: 1, bar: 2, baz: 3], string>也会如此[foo: string]。这让我可以Message在接下来的内容中保留您的标签。


让我们试一试:

type Message = Optionalize<MessageMarker>;

IntelliSense,如果它没有截断东西,会给你这个:

/* type Message = 
  [level: Level, message: string] | 
  [level: Level, message: string, mergeable: boolean] | 
  [level: Level, displayLevelDefaultTitle: boolean, message: string] | 
  [level: Level, displayLevelDefaultTitle: boolean, message: string, mergeable: boolean] |
  [relatedField: string, level: Level, message: string] | 
  [relatedField: string, level: Level, message: string, mergeable: boolean] | 
  [relatedField: string, level: Level, displayLevelDefaultTitle: boolean, message: string] | 
  [relatedField: string, level: Level, displayLevelDefaultTitle: boolean, message: string, mergeable: boolean] | 
  [visible: boolean, level: Level, message: string] | 
  [visible: boolean, level: Level, message: string, mergeable: boolean] | 
  [visible: boolean, level: Level, displayLevelDefaultTitle: boolean, message: string] | 
  [visible: boolean, level: Level, displayLevelDefaultTitle: boolean, message: string, mergeable: boolean]
  [visible: boolean, relatedField: string, level: Level, message: string] | 
  [visible: boolean, relatedField: string, level: Level, message: string, mergeable: boolean] | 
  [visible: boolean, relatedField: string, level: Level, displayLevelDefaultTitle: boolean, message: string] | 
  [visible: boolean, relatedField: string, level: Level, displayLevelDefaultTitle: boolean, message: string, mergeable: boolean]
*/

如您所见,这是一个由 16 名成员组成的工会,您不必手动写出。现在您的示例代码都按预期工作。此外,赋值缩小允许编译器知道您将 16 个成员中的哪一个用于任何给定值:

const simpleMessage: Message = ["s", "user saved"];
// Success: user saved.
// const simpleMessage: [level: Level, message: string]

const hiddenMessage: Message = [false, "i", "CPU usage for this operation: 0.1%"];
// <hidden> Information: CPU usage for this operation: 0.1%.
// const hiddenMessage: [visible: boolean, level: Level, message: string]

万岁,我猜!


结语:所以,你可以跳过一堆类型杂耍的圈子,把你想写的元组之类的东西转换成你可以写的元组并集。在那之后,您仍然必须处理函数实现。如果你决定走这条路,我不羡慕你。尽管这样的事情是可能的,但它似乎并不是 TypeScript 支持的用例。

Playground 代码链接


推荐阅读