为什么 TypeScript 不检查动态键对象字段的类型

Why is TypeScript Not Checking the Type of Dynamic Key Object Fields

为什么 TypeScript 接受 seta 的定义而不接受 return 类型 A 的对象?

type A = {
    a: '123',
    b: '456'
}

// Returns copy of obj with obj[k] = '933'
function seta<K extends keyof A>(k: K, obj: A): A {
    return {
        ...obj,
        [k]: '933'
    }
}

const w: A = seta('a', {
    a: '123',
    b: '456'
})

// now w = {a: '933', b: '456'}

https://tsplay.dev/wEGX4m

它已被接受,因为如果您将鼠标悬停在 [k]: '933', 上,它会显示

(parameter) k: K extends keyof A

这意味着您正在 returning 一个从 return 类型扩展而来的 属性,这是允许的。

当你声明一个对象必须是一个接口时,你是说它必须至少有那些属性,而不仅仅是那些属性。

这看起来像是 TypeScript 中的错误或限制;参见 microsoft/TypeScript#37103 (labeled a bug) or microsoft/TypeScript#32236(标记为“需要调查”)。

当你 spread properties into an object literal and then add a computed property, it seems that TypeScript will completely ignore the computed property unless the computed key is of a single, specific, literal type (not a union of literals, and not a generic type parameter constrained 一个字符串字面值时):

function testing<K extends "a" | "b">(a: "a", x: string, y: "a" | "b", z: K) {
    const someObject = { a: "v" } // { a: string }
    const objA = { ...someObject, [a]: 0 } // { a: number } 
    const objX = { ...someObject, [x]: 0 } // { a: string } 
    const objY = { ...someObject, [y]: 0 } // { a: string } 
    const objZ = { ...someObject, [z]: 0 } // { a: string } 
}

计算的 属性 键通常在 TypeScript 中有点问题,即使没有传播,因为它们往往会一直扩大到 string,丢失您可能关心的信息(参见另一个microsoft/TypeScript#13948 处的错误):

function testing2<K extends "a" | "b">(a: "a", x: string, y: "a" | "b", z: K) {
    const objA = { [a]: 0 } // { a: number } 
    const objX = { [x]: 0 } // { [x: string]: number; } ‍♂️
    const objY = { [y]: 0 } // { [x: string]: number; } ‍♂️
    const objZ = { [z]: 0 } // { [x: string]: number; } ‍♂️
}

这就是您遇到的问题。


除了“谨慎对待计算属性”之外,我不知道该说些什么。您可以通过定义自己的函数来解决它,该函数生成计算出的 属性 的更“正确”版本:

function kv<K extends PropertyKey, V>(k: K, v: V) {
    return { [k]: v } as { [P in K]: { [Q in P]: V } }[K]
}

const k = Math.random() < 0.5 ? "a" : "b";

const obj = kv(k, Math.random());
/* const obj: {
    a: number;
} | {
    b: number;
} */

您可以看到键的并集产生对象的并集。但是,如果密钥是通用的,那么编译器将不对它进行评估,并且使用泛型进行传播往往表示为该问题的 intersection, which is often acceptable but not when keys overlap. (See microsoft/TypeScript#32022):

function setA2<K extends keyof A>(k: K, obj: A): A {
    return {
        ...obj,
        ...kv(k, '933')
    } 
    /* const ret1: A & { [P in K]: { [Q in P]: string; }; }[K] */ // no error
}

因此,在收到任何警告之前,您必须将 kK 扩大到 keyof A

function setA3<K extends keyof A>(k: K, obj: A): A {
    const kW: keyof A = k;
    return {
        ...obj,
        ...kv(kW, '933')
    }; // FINALLY AN ERROR
    // Type '{ a: string; b: "456"; } | { b: string; a: "123"; }' is not assignable to type 'A'.
}

所以,万岁,我想...您可以花很多精力从这个函数中争取一些类型安全,但这是一个代价高昂的胜利(我 think 就是一个词)。因此,在相关的 GitHub 问题得到修复之前,请再次注意计算属性。

Playground link to code