为什么 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'}
它已被接受,因为如果您将鼠标悬停在 [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
}
因此,在收到任何警告之前,您必须将 k
从 K
扩大到 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 问题得到修复之前,请再次注意计算属性。
为什么 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'}
它已被接受,因为如果您将鼠标悬停在 [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
}
因此,在收到任何警告之前,您必须将 k
从 K
扩大到 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 问题得到修复之前,请再次注意计算属性。