在下面的TypeScript代码片段中,我需要从一个对象分配到另一个对象,其中两个对象都是Partial<InjectMap>
。在这里,我的直觉是打字脚本应该能够理解发生了什么,因为在第(B(行,key
的类型是typeof InjectMap
。因此,它应该能够正确地分配从input
到output
的值。
export interface InjectMap {
"A": "B", // line (A)
"C": "D"
}
type InjectKey = keyof InjectMap;
const input: Partial<InjectMap> = {};
const output: Partial<InjectMap> = {};
const keys: InjectKey[] = []
for (let i = 0; i < keys.length; i++) {
const key = keys[i] // line (B)
output[key] = input[key] // line (C) - Gives Error
}
游乐场链接
但它在第(C(行给出了以下错误:
Type '"B" | "D" | undefined' is not assignable to type 'undefined'.
Type '"B"' is not assignable to type 'undefined'.
奇怪的是,如果我评论第(A(行,错误就会消失。这是TypeScript的一个缺点,还是我遗漏了什么?
我不认为这是一个错误,更改值几乎总是不安全的,TS只是试图使其安全。
让我们从InjectMap
接口开始。
很明显,你不能拥有非法状态,比如:
const illegal: InjectMap = {
"A": "D", // expected B
"C": "B" // expected D
}
这很重要。
让我们继续我们的循环:
interface InjectMap {
"A": "B",
"C": "D"
}
type InjectKey = keyof InjectMap;
const input: Partial<InjectMap> = {};
const output: Partial<InjectMap> = {};
const keys: InjectKey[] = []
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const inp = input[key] // "B" | "D" | undefined
const out = output[key] // "B" | "D" | undefined
output[key] = input[key]
}
由于key
是动态的,TS不确定它是B
、D
还是undefined
。我希望你同意我的观点,在这个地方inp
的正确类型是"B" | "D" | undefined
,这是预期的行为,因为类型系统是静态的。
由于input
和output
不被key
绑定,TS希望避免非法状态。为了说明这一点,请考虑下一个例子,它与我们的相同
type KeyType_ = "B" | "D" | undefined
let keyB: KeyType_ = 'B';
let keyD: KeyType_ = 'D'
output[keyB] = input[keyD] // Boom, illegal state! Runtime error!
您可能已经注意到,keyB
和keyD
具有相同的类型,但值不同。
与您在示例中遇到的情况相同,TS无法计算出它只能计算出类型的值。
如果你想让TS高兴,你应该添加条件语句或typeguard:
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (key === 'A') {
let out = output[key] // "B"
let inp = input[key] // "B"
output[key] = input[key] // ok
}
if (key === 'C') {
let out = output[key] // "D"
let inp = input[key] // "D"
output[key] = input[key] // ok
}
}
请记住,当你改变你的价值观时,你就会失去类型保证。
看看这个和这个关于突变的问题。
提香·德拉戈米尔·切尔尼科娃的这段话也很不错。
这里有一个不安全突变的例子,取自@Titian的演讲:
type Type = {
name: string
}
type SubTypeA = Type & {
salary: string
}
type SubTypeB = Type & {
car: boolean
}
type Extends<T, U> =
T extends U ? true : false
let employee: SubTypeA = {
name: 'John Doe',
salary: '1000$'
}
let human: Type = {
name: 'Morgan Freeman'
}
let director: SubTypeB = {
name: 'Will',
car: true
}
// same direction
type Covariance<T> = {
box: T
}
let employeeInBox: Covariance<SubTypeA> = {
box: employee
}
let humanInBox: Covariance<Type> = {
box: human
}
// Mutation ob object property
let test: Covariance<Type> = employeeInBox
test.box = director // mutation of employeeInBox
const result_ = employeeInBox.box.salary // while result_ is undefined, it is infered a a string
// Mutation of Array
let array: Array<Type> = []
let employees = [employee]
array = employees
array.push(director)
const result = employees.map(elem => elem.salary) // while salary is [string, undefined], is is infered as a string[]
console.log({result_,result})
游乐场
如何修复
请告诉我它是否适合你:
export interface InjectMap {
"A": "B",
"C": "D"
}
const assign = <Input extends InjectMap, Output extends InjectMap>(
input: Partial<Input>,
output: Partial<Output>,
keys: Array<keyof InjectMap>
) => keys.reduce((acc, elem) => ({
...acc,
[elem]: input[elem]
}), output)
游乐场
更新
为什么此分析适用于[elem]:input[elem]?输入[elem]可以再次是"0";B"|"D"|未定义,因此编译器应该再次出错。但是,在这里,编译器很聪明地知道输入[elem]的类型适用于[elem]。有什么区别?
这是一个很好的问题。
当你用计算键创建新对象时,TS会使这个对象成为indexed by string
,我的意思是,你可以使用任何想要的字符串prop
const computedProperty = (prop: keyof InjectMap) => {
const result = {
[prop]: 'some prop' // { [x: string]: string; }
}
return result
}
它给你更多的自由,但也提供了一点不安全。
拥有强大的权力,就肩负着巨大的责任
因为现在,不幸的是,你可以这样做:
const assign = <Input extends InjectMap, Output extends InjectMap>(
input: Partial<Input>,
output: Partial<Output>,
keys: Array<keyof InjectMap>
) => keys.reduce((acc, elem) => {
return {
...acc,
[elem]: 1 // unsafe behavior
}
}, output)
正如您可能已经注意到的,assign
函数的返回类型是Partial<Output>
,这不是真的。
因此,为了使其完全类型安全,您可以使用typeguard,但我认为这会使过于复杂