网站搜索

typescript - 无法使用“联合”索引类型正确索引对象


首先,我不太确定我的代码出了什么问题,所以如果有更好的标题建议,我会修改,谢谢。

我尝试使用“key”索引一个对象,而 key 是联合类型,如果 key 的类型被缩小,没有问题,但如果类型是联合类型,TS 就不高兴,这是为什么?

我也不明白 TS 报的错误,这个交叉类型是从哪来的?

操场

type FetchedData = {
    A?: {
        A:'A',
        otherField: string
    },
    B?: {
        B:'B',
        otherField: string
    },
    C?: {
        C:'C',
        otherField: string
    },
};

type CachedData = {
    A: {
        A:'A'
    },
    B: {
        B:'B'
    },
    C: {
        C:'C'
    },
};

const cachedData: CachedData = {
    A: {
        A:'A'
    },
    B: {
        B:'B'
    },
    C: {
        C:'C'
    },
}

type Key = keyof FetchedData;

const key: Key = Math.random() > 0.6 ? 'A' : Math.random() > 0.3 ? 'B' : 'C';

const fetchedData: FetchedData = {};

switch (key) {
    case "A" : {
        const cache = cachedData[key]
        fetchedData[key] = {...cache, otherField: 'otherField'}
    }
    // the above is okay, but te below is not, why?
    default : {
        const cache = cachedData[key]
        /* 
            const cache: {
                A: 'A';
            } | {
                B: 'B';
            } | {
                C: 'C';
            }
        */
        fetchedData[key] = {...cache, otherField: 'otherField'}
        /* 
            Type '{ otherField: string; A: "A"; } | { otherField: string; B: "B"; } | { otherField: string; C: "C"; }' is not assignable to type '({ A: "A"; otherField: string; } & { B: "B"; otherField: string; } & { C: "C"; otherField: string; }) | undefined'.
            Type '{ otherField: string; A: "A"; }' is not assignable to type '{ A: "A"; otherField: string; } & { B: "B"; otherField: string; } & { C: "C"; otherField: string; }'.
            Property 'B' is missing in type '{ otherField: string; A: "A"; }' but required in type '{ B: "B"; otherField: string; }'.ts(2322)
        
        */

        // I don't understand why TS says "... not assignable to type '({ A: "A"; otherField: string; } & { B: "B"; otherField: string; } & ..." where does that intersection come from?
    }
}

答案#1

TypeScript 并不真正支持 相关联合类型。由于 key 是联合类型,所以 const cache=cachedData[key] 和 fetchedData[key]={...cache, otherField: 'otherField'} 都涉及联合类型。但编译器只是根据它们的 类型 来独立处理它们,而不是根据 key 的身份。想象一下,如果你有 key1 和 key2,它们的联合类型和 key 相同。你不会希望编译器接受 fetchedData[key1]={。 ..cachedData[key2], otherField: 'otherField'},对吧?因为虽然 key2 和 key1 拥有相同的联合类型,但它们可能不是相同的值。而从类型系统的角度来看,根本无法区分你的代码与这种场景。交集的出现源于编译器说明,要确保这样的赋值在任何情况下都是安全的,需要做哪些操作,无论 key1 和 key2 是什么。参见 microsoft/TypeScript#30769 获取引入该交集行为的 PR 以及它的工作原理。

原来是这个问题。你的代码可能没问题,但编译器无法理解逻辑。


如果你只是想做最便捷的事情,就接受你比编译器更聪明,并使用类型断言来抑制错误:

const cache = cachedData[key]
fetchedData[key] = { ...cache, otherField: 'otherField' } as any

否则:

如果你打算使用联合类型,那么你需要使用逐案分析,比如 switch/case 或 if/else 来在操作之前将 key 缩小到各个联合成员……单独一段代码是行不通的。如果你想使用单独一段代码,那么你需要从联合类型切换到受限于联合的泛型。具体的重构技术在 microsoft/TypeScript#47109 中有描述,作为支持相关联合的一种方法。

在示例代码中,它可能看起来像这样。 从你的基础 CachedData 类型开始:

type CachedData = {
    A: { A: 'A' },
    B: { B: 'B' },
    C: { C: 'C' },
};
type Key = keyof CachedData;
const cachedData: CachedData = {
    A: { A: 'A' },
    B: { B: 'B' },
    C: { C: 'C' },
}

然后将 FetchedData 表示为 CachedData 上的映射类型,并在你关心的键的子集上使其成为泛型:

type FetchedData<K extends Key = Key> =
    { [P in K]?: CachedData[P] & { otherField: string } }
const fetchedData: FetchedData = {};

最后,我们可以将你的单一代码块写成一个关于 K extends Key 的通用函数体:

function f<K extends Key>(key: K) {
    const cache = cachedData[key];
    // const cache: CachedData[K]
    const _fetchedData: FetchedData<K> = fetchedData;
    const v = { ...cache, otherField: 'otherField' };
    // const v: CachedData[K] & { otherField: string; }
    _fetchedData[key] = v;
}

之所以能编译,是因为 cache 是泛型类型 CachedData[K],而 _fetchedData[key] 被视为泛型类型 CachedData[K] & { otherField: string }。这两者都不是联合类型。并且值 {...cache, otherField:'otherField'} 被视为与 _fetchedData[key] 相同的类型 CachedData[K] & { otherField: string }。

最后,你可以使用联合类型的 key 作为输入来调用你的函数:

const key: Key =
    Math.random() > 0.6 ? 'A' : Math.random() > 0.3 ? 'B' : 'C';

f(key); // okay

为了让这里的编译器满意,需要做相当多的繁琐操作。你必须重写一些类型,并将一些值复制到适当的类型。就我个人而言,我可能会直接使用类型断言,而不会去麻烦进行重构,除非我确实有特别的需求,要优先使用编译器验证的类型而不是图方便。

代码的 Playground 链接