如何根据提供的类型在 TypeScript 中创建可区分联合的实例?

How to create an instance of a discriminated union in TypeScript based on a provided type?

鉴于可区分联合 MyUnion,我想调用一个函数 createMyUnionObject,其中包含 MyUniontype 之一和 [=15= 的值] 必须是正确的类型。

type MyUnion =
    {
        type: 'one'
        value: string
    } |
    {
        type: 'two',
        value: number
    } |
    {
        type: 'three',
        value: boolean
    };

function createMyUnionObject<
    T extends MyUnion['type'],
    MU extends Extract<MyUnion, { type: T }>,
    V extends MU['value']
>(type: T, value: V): MU {
    // Real code is conditional and may modify `value`, this is a simple example with the same problem I'm having
    return { type, value };
}

如果您尝试调用 createMyUnionObject,您会发现 TypeScript 正在正确推断函数定义之外的值。也就是说,createMyUnionObject('one', false) 将出现所需的编译错误 Argument of type 'false' is not assignable to parameter of type 'string'.

但是,在函数定义中,我看到了 Type '{ type: T; value: V; }' is not assignable to type 'MU'. 但我不明白为什么 - 我怎样才能构造对象以使 TypeScript 满意它是正确的类型?

TypeScript 目前无法通过控制流分析来缩小泛型类型参数的类型。理想情况下,我希望能够像这样编写您的函数:

function createMyUnionObject<T extends MyUnion["type"]>(
    type: T,
    value: Extract<MyUnion, { type: T }>["value"]
): Extract<MyUnion, { type: T }> {
    return { type, value }; // error 
}

但我们不能(请注意,我们应该只需要一个通用参数......从 T 你应该能够计算 value 和 return 类型)。即使对于 type 及其对应的 value 的任何具体值,编译器都可以验证 {type, value} 是否可分配给 MyUnion,但它不能用泛型来做到这一点。


一个障碍是编译器怀疑你的 type 可能是完整联合类型 "one" | "two" | "three",当然 value 因此可能是 string | number | boolean,并且那么 {type, value} 可能 not 可以分配给 MyUnion:

const t = (["one", "two", "three"] as const)[Math.floor(Math.random() * 3)];
const v = (["one", 2, true] as const)[Math.floor(Math.random() * 3)];
createMyUnionObject(t, v); // uh oh

你要告诉编译器:不行,T只能是或者"one"或者 "two" "three",但不是其中两个或多个的并集。那还不是语言的一部分,但它是 been requested. If that ever gets implemented it would still need to be combined with some kind of control flow analysis that did multiple passes over the body doing each possible narrowing in turn. I've suggested this (and related things) 但同样,它还不是语言的一部分。


现在,我会说,如果你想让你的代码编译并继续,你需要使用 type assertion 来告诉编译器你知道你在做什么是安全的(而且你不会担心有人会像上面的代码那样疯狂地做 Math.random() 事情):

function createMyUnionObject<T extends MyUnion["type"]>(
    type: T,
    value: Extract<MyUnion, { type: T }>["value"]
) {
    return { type, value } as any as Extract<MyUnion, { type: T }>; // assert
}

这应该可行,您可以根据需要使用 createMyUnionObject()。好的,希望有帮助。祝你好运!

Link to code