我有一个存储一组区分的联合类型的类型:

type M = {
    foo: {a: "foo"};
    bar: {a: "bar"};
};

现在我想创建一个函数,根据它的参数构造并返回正确类型的实例,比如:

const a1 = f1("foo"); // {a: "foo"}
const b1 = f1("bar"); // {a: "bar"}

但看似简单的正向类型赋值未通过类型判断:

function f1<T extends keyof M>(a: T): M[T] {
    return {a};
    /*
    Type '{ a: keyof M; }' is not assignable to type 'M[T]'.
        Type '{ a: keyof M; }' is not assignable to type 'never'.
        The intersection '{ a: "foo"; } & { a: "bar"; }' was reduced
         to 'never' because property 'a' has conflicting types in some
         constituents.(2322)
    */
}

然而,如果我这样做,它就会工作(我仍然需要创建new个实例,所以它对我没有帮助):

const v = {
    foo: {a: "foo" as const},
    bar: {a: "bar" as const},
};

type V = typeof v;

function f2<T extends keyof V>(a: T): V[T] {
    return v[a]; // no problem!
}

const a2 = f2("foo"); // {a: "foo"}
const b2 = f2("bar"); // {a: "bar"}

即使类型V与上面的类型M相同.

让它更令人困惑的是,这一点也失败了:

const c = {
    foo: {x: {a: "foo" as const}},
    bar: {x: {a: "bar" as const}},
};

function f<T extends keyof M>(a: T): M[T] {
    return c[a].x;
    /*
        Type '{ a: "foo"; } | { a: "bar"; }' is not assignable to type
         'M[T]'.
        Type '{ a: "foo"; }' is not assignable to type 'M[T]'.
            Type '{ a: "foo"; }' is not assignable to type 'never'.
            The intersection '{ a: "foo"; } & { a: "bar"; }' was reduced
             to 'never' because property 'a' has conflicting types in
             some constituents.(2322)
    */
}

为什么TS会在第一个和第三个示例中try 与所有并集类型相交,而不是在第二个示例中?我是不是遗漏了什么?有没有办法让它发挥作用?

Playground link

推荐答案

以下方法奏效的原因:

function f2<K extends keyof V>(k: K): V[K] {
    return v[k];
}

是因为您正在执行已知与indexed access type兼容的基本操作.如果使用类型K的键k索引到类型V的值v,则会得到索引访问类型V[K]的索引访问值v[k].

现在,至于为什么您的其他代码不能工作:


TypeScrip不会多次分析一段代码.所以它不能看着身体

function f1<K extends keyof M>(a: T): M[K] {
    return {a}; 
}

然后说"如果K等于"foo"呢好吧,那行. 如果K等于"bar"呢? 这也行这就排除了所有的可能性,所以这意味着整个函数都是类型良好的.我同意.我会把这种类型判断算法称为"分布式control-flow analysis",如果编译器自动做这些事情,它会导致灾难性的编译速度减慢,因为它最终会分析一些代码块数百或数千次. 也没有办法 Select 这样的分析;在一个点上,我曾要求在microsoft/TypeScript#25051,但它被拒绝.


铅字也不能从头开始在字体中使用deduce种图案.所以它不能看着类型M然后注意到,"哦,嘿,看.类型{foo: {a: "foo"}, bar: {a: "bar"}}可以被抽象出来,这样每个属性P都是{a: P}的形式,这意味着M[K]基本上与{a: K}相同,我可以在它们之间随意转换."当您编写return {a}时,编译器知道它的类型为{a: K},但现在它执行not,因为这与M[K]相同.这种联系超出了编译器的推理能力.

所以编译器只能这样推理:"代码需要返回M[K],它是{a: "foo"}还是{a: "bar"},但我不知道哪一个是正确的.实际的返回值是{a: K}类型,但我看不出它和期望的返回值之间有任何关系.所以我唯一能确定的是安全返回的东西总是它是both{a: "foo"}and{a: "bar"},也就是intersection{a: "foo"} & {a: "bar"}.{a: K}是可赋值的吗?不是.(实际上,没有赋值给它.)太糟糕了.它被损坏了,我会报告一个错误."

交集具体来自microsoft/TypeScript#30769中的Pull请求,但这是基本逻辑.


这就是你的破解代码被破解的原因.microsoft/TypeScript#30581中描述了编译器通常无法跟踪这样的类型之间的相关性,microsoft/TypeScript#47109中描述了修复该问题的一般方法.修复涉及到重构到特定类型的generic方法,在这种方法中,对象用基本映射类型表示,所有操作都是这些类型的泛型索引或这些类型上的mapped types索引.

只需显式地将c的类型作为映射类型超过M,就可以使第三个函数工作.

const c: { [K in keyof M]: { x: M[K] } } = {
    foo: { x: { a: "foo" } },
    bar: { x: { a: "bar" } },
};   

function f<K extends keyof M>(a: K): M[K] {
    return c[a].x; // okay
}

这是一个等价的类型,但形式不同.您不仅希望编译器能注意到cM之间的关系,还希望显式地将其写出来.因此,现在已知c[a]是类型{x: M[K]},因此c[a].x是类型M[K].

您的第一个函数比较难修复,因为没有一种简单的方法可以让编译器满意地使用M.相反,我只想说,我们将构建一个名为Ndistributive object type(请阅读该术语的MS/TS#47109),它捕获了泛型类型a{ a }:

type N<K extends keyof M> = { [P in K]: { a: P } }[K]
function f1<K extends keyof M>(a: K): N<K> {
    return { a };
}

你可能能够也可能不能在下游使用那些关心M的东西.


Playground link to code

Typescript相关问答推荐

将对象属性路径描述为字符串数组的类型

如何将http上下文附加到angular中翻译模块发送的请求

如何在方法中定义TypeScript返回类型,以根据参数化类型推断变量的存在?

类型脚本接口索引签名

我用相同的Redux—Toolkit查询同时呈现两个不同的组件,但使用不同的参数

如何访问Content UI的DatePicker/中的sx props of year picker?'<>

访问继承接口的属性时在html中出错.角形

Angular NgModel不更新Typescript

使用2个泛型类型参数透明地处理函数中的联合类型

函数重载中的不正确TS建议

在Mac和Windows上运行的Web应用程序出现这种对齐差异的原因是什么?(ReactNative)

如何将spread operator与typescripts实用程序类型`参数`一起使用

Typescript将键数组映射到属性数组

如何使用TypeScrip在EXPO路由链路组件中使用动态路由?

如何键入并类型的函数&S各属性

TypeScrip:如何缩小具有联合类型属性的对象

扩展对象的此内容(&Q;)

对象只能使用 Typescript 中另一个对象的键

在Nestjs中,向模块提供抽象类无法正常工作

在 next.js 13.4 中为react-email-editor使用forwardRef和next/dynamic的正确方法