混合联合类型,泛型和条件类型会导致意外的“类型无法分配给类型”错误

时间:2020-09-19 23:37:11

标签: typescript generics typescript-generics union-types conditional-types

我遇到了类型推断问题,特别是在联合类型中使用条件类型时。

可能有一种较短的方法来演示此问题,但是我找不到一个...

在此playground link上查看问题所在。

请考虑以下Result<T>,这是一个联合类型,用于指示操作的成功或失败(带有可选的附加值,类型为T)。对于成功案例,我使用了条件类型SuccessResult<T>,该条件类型解析为OKResultValueResult<T>(取决于结果是否还应带有附加的value) :

type Result<T = undefined> = SuccessResult<T> | ErrorResult;

interface OKResult {
    type: 'OK';
}
interface ValueResult<T> {
    type: 'OK';
    value: T;
}
interface ErrorResult {
    type: 'Error';
    error: any;
}
type SuccessResult<T = undefined> = T extends undefined ? OKResult : ValueResult<T>;

function isSuccess<T>(result: Result<T>): result is SuccessResult<T> {
    return result.type === 'OK';
}

我们将其与简单的联合类型一起使用:

type C1 = "A1" | "B1";
function makeC1(): C1 { return "A1" }
const c1: C1 = makeC1();
const c1Result: Result<C1> = { type: "OK", value: c1 }; // ALL IS GOOD

现在,让我们以完全相同的方式使用复杂值的联合类型C1来代替简单的联合类型"A1" | "B1"(即C2):

type A2 = {
    type: 'A2';
}
type B2 = {
    type: 'B2';
}
type C2 = A2 | B2;
function makeC2(): C2 { return { type: "A2" } }
const c2: C2 = makeC2();
const c2Result: Result<C2> = { type: "OK", value: c2 }; // OH DEAR!

这会导致错误:

类型'C2'不能分配给类型'B2'。

类型'A2'不能分配给类型'B2'。

“类型”属性的类型不兼容。

类型'“ A2”'无法分配给类型'“ B2”'。

如果我从等式中删除条件类型并定义我的Result<T>以使用ValueResult<T>而不是SuccessResult<T>

type Result<T = undefined> = ValueResult<T> | ErrorResult;

...一切再次正常,但是我失去了表示无价值成功的能力。如果在这种情况下无法使可选类型正常工作,那将是一个可悲的后退。

我哪里出错了?在SuccessResult<T>本身是复杂的联合类型的情况下,如何在Result<T>联合中使用T

Playground link

4 个答案:

答案 0 :(得分:1)

type Result<T = undefined> = SuccessResult<T> | ErrorResult;

需要成为

type Result<T = undefined> = SuccessResult<T> | ErrorResult | ValueResult<T>;

然后编译。

干杯,迈克

答案 1 :(得分:1)

不幸的是,仅指定函数的返回值还不够。您需要显式返回类型。然后编译。

无效

function makeC2(): C2 {
    return {
        type: "A2"
    };
};

有效

function makeC2() {
    const x: C2 = {
        type: "A2"
    };
    return x;
};

Playground link

答案 2 :(得分:1)

之所以会这样,是因为C2A2 | B2类型,而Result<C2>{type: 'OK'; value: A2} | {type: 'OK'; value: B2} | ErrorResult

因此,问题归结为{type: 'OK'; value: A2 | B2}{type: 'OK'; value: A2} | {type: 'OK'; value: B2}的等效性。在您向此类对象添加更多属性之前,这似乎是等效的。 TypeScript不会将其视为特殊情况,而是考虑不等效的一般情况(想象更多属性)。

让我们考虑另一个示例{ x: "foo" | "bar" }{ x: "foo" } | { x: "bar" }

例如,认为{ x: "foo" | "bar", y: string | number }等同于{ x: "foo", y: string } | { x: "bar", y: number }是不正确的,因为第一种形式允许所有四种组合,而第二种形式仅允许两种特定的组合。

来源:https://github.com/microsoft/TypeScript/issues/12052#issuecomment-258653766

答案 3 :(得分:1)

正如某些答案中已经提到的那样,问题在于您有A2B2这两种不同类型的对象,尽管它们在您的情况下是相同的(两者都具有属性{ {1}}),您可以轻松地将其他属性附加到一个属性,而不是其他属性,在这种情况下,如果type后面的对象实际上是正确的,则Result<C2>将不会有所不同将无法分配给A2,反之亦然。

您可以做的(如果结果类型具有相同的属性)是创建某种基本类型,其中B2实际上是一个通用联合,它将解析为以下特定值之一:

type

或更简单的版本

type BaseType<T extends 'A2' | 'B2'> = {
    type: T;
}

type A2 = BaseType<'A2'>
type B2 = BaseType<'B2'>
type C2 = A2 | B2;

function makeC2(): C2 { return { type: 'A2' } }
const c2 = makeC2();
const c2Result: Result<BaseType<'A2' | 'B2'>> = { type: "OK", value: c2 };

这取决于您的情况以及是否可以共享这样的类型,或者它们是否特定。