避免 TypeScript 标准化对象字面量联合的方法

问题描述 投票:0回答:1

考虑这段代码:

function Foo(num: number) {
  switch (num) {
    case 0: return { type: "Quz", str: 'string', } as const;
    case 1: return { type: "Bar", 1: 'value' } as const;
    default: throw new Error("Unknown discriminant: " + num);
  }
}

打字稿推断出这个可区分的联合类型:

function Foo(num: number): 
{ readonly type: "Quz"; readonly str: "string"; readonly 1?: undefined; } |
{ readonly type: "Bar"; readonly 1: "value"; readonly str?: undefined; }

但是我不想得到打字稿推断的歧视性联盟类型,但期望这样:

{ type: "Quz"; str: "string"; } | { type: "Bar"; 1: "value"; }

我不想单独指定返回类型。另外,我不想提前评估任何可能的输出。

有没有办法提示打字稿编译器猜测我期望的判别联合的类型?

typescript
1个回答
6
投票

TypeScript 在推断值的类型时使用各种启发式规则,选择这些规则是为了为广泛的用例提供理想的行为......但总会有一些人和情况下启发式不可避免地无法满足期望。

其中一个规则是,对象文字的联合是通过联合其他成员的可选

undefined
属性推断出来的,如 TypeScript 2.7 发行说明 中所述以及 microsoft/TypeScript#19513 中的实现。 这使得原本难以处理的联合(例如
{foo: string} | {}
,您无法使用
foo
进行索引)变成了 受歧视联合(例如
{foo: string} | {foo?: undefined}
)。 但不幸的是,对于您的示例,这会导致您不想要的类型。


一般来说我建议有人手动注释他们期望的类型,如果推理不能按他们想要的方式工作,编译器将使用它作为上下文

type DiscU = { type: "Quz"; str: "string"; } | { type: "Bar"; 1: "value" };

function fooAnnotate(num: number): DiscU {
    switch (num) {
        case 0: return { type: "Quz", str: 'string', }; 
        case 1: return { type: "Bar", 1: 'value' };
        default: throw new Error("Unknown discriminant: " + num);
    }
}

在上面,输出类型正是您想要的(因为它是手动注释的),您甚至不需要

const
断言,因为所需的类型会在上下文中键入这些对象文字。

不幸的是,要求您不要手动写出类型,因此我们将放弃这种方法。


因为您不喜欢的行为仅在使用对象字面量时发生,所以“标准”解决方法与您用来避免过多属性检查的解决方法相同:将对象字面量分配给中间变量,然后构建与变量联合:

function foo(num: number) {
    const case0 = { type: "Quz", str: 'string' } as const;
    const case1 = { type: "Bar", 1: 'value' } as const;
    switch (num) {
        case 0: return case0;
        case 1: return case1;
        default: throw new Error("Unknown discriminant" + num);
    }
}
/* function foo(num: number): {
    readonly type: "Quz";
    readonly str: "string";
} | {
    readonly type: "Bar";
    readonly 1: "value";
} */

这将为您提供预期的类型(好吧,除了属性是

readonly
;我们稍后会回来讨论......)

万岁! 但这涉及到预先计算每个可能输入的输出,并且要求您不要这样做。 所以我们需要修改这种方法。


避免预计算的一种方法是用返回该值的立即执行函数替换该值。 您可以内联执行此操作,甚至将其移回到

switch
/
case
语句中:

function foo(num: number) {
    switch (num) {
        case 0: return (() => ({ type: "Quz", str: 'string' } as const))();
        case 1: return (() => ({ type: "Bar", 1: 'value' } as const))();
        default: throw new Error("Unknown discriminant" + num);
    }
}
/* function foo(num: number): {
    readonly type: "Quz";
    readonly str: "string";
} | {
    readonly type: "Bar";
    readonly 1: "value";
} */

中间函数设法阻止可选的

undefined
属性,并满足您的其余需求。 万岁!


嗯,属性仍然是

readonly
。 如果这很重要,您可以将立即执行的函数更改为独立函数,该函数返回其输入的非
readonly
版本(使用带有-readonly
修饰符
映射类型):

function foo(num: number) {
    const mutable = <T extends object>(o: T): { -readonly [K in keyof T]: T[K] } => o;
    switch (num) {
        case 0: return mutable({ type: "Quz", str: 'string' } as const);
        case 1: return mutable({ type: "Bar", 1: 'value' } as const);
        default: throw new Error("Unknown discriminant" + num);
    }
}
/* function foo(num: number): {
    type: "Quz";
    str: "string";
} | {
    type: "Bar";
    1: "value";
} */

现在您已经完全得到了您想要的类型,无需将其写出或预先计算函数的任何输出值。 😅

Playground 代码链接

© www.soinside.com 2019 - 2024. All rights reserved.