Reputation: 95
interface A {
name?: string
age: number
}
var a: A = {
name: '',
age: 23
}
var result:A = (Object.keys(a) as Array<keyof A>).reduce((prev, key) => {
if (a[key] || a[key] === 0) {
prev[key] = a[key] // this reported a error about `Type 'undefined' is not assignable to type 'never'`
}
return prev
}, {} as A)
console.log(JSON.stringify(result))
above is reproduce code.
I found that the code works under typescript@~3.4.0,but does not compileunder typescript@^3.5.0, and I checked the update log between 3.4 and 3.5,but I did not find any references about this.
So I guess if it is because index signature
is not set, then:
interface A {
name?: string
age: number
[K:string]:any <-- add this line
}
var a: A = {
name: '',
age: 23
}
var result:A = (Object.keys(a) as Array<keyof A>).reduce((prev, key/* validation lost */) => {
if (a[key] || a[key] === 0) {
prev[key] = a[key]
}
return prev
}, {} as A)
console.log(JSON.stringify(result))
The previous error disappeared, but the type of key
that was the parameter in the reduce
callback became string|number
, causing the type validation of the key to be lost.
Is this the normal behavior?
if yes, I wonder that how to solve Type 'undefined' is not assignable to type 'never'
,and keep the type check for the key
.
Upvotes: 5
Views: 3345
Reputation: 74800
In TS 3.5 there actually happened a breaking change with the PR Improve soundness of indexed access types:
When an indexed access T[K] occurs on the source side of a type relationship, it resolves to a union type of the properties selected by T[K], but when it occurs on the target side of a type relationship, it now resolves to an intersection type of the properties selected by T[K]. Previously, the target side would resolve to a union type as well, which is unsound.
Carried over to your example, prev[key] = a[key]
now emits an error, because key
has the union type "name" | "age"
and prev[key]
(the target side of the assignment) resolves to an intersection type of all selected properties: A["name"] & A["age"]
, which is string & number
or in other words never
(with prev
of type A
).
The thought behind this inferred intersection for prev[key]
is to ensure that all possible keys "name" | "age"
of prev
can be safely written to. If the run-time value of key
is age
, it would be an error to write a string
(the expected property type of name
) to it. At compile time with type keyof A
, we don't know, what the exact value of key
is, so the changes in the PR enforce safer types.
The solution is to introduce generic type parameters for the object (prev
) and/or property name(key
). Some examples are given here, here and here by the maintainers. I am not sure about your use case, but for example you could rewrite the code like this:
const result: A = (Object.keys(a) as Array<keyof A>).reduce(
<K extends keyof A>(prev: A, key: K) => {
// t[key] === 0 would only work for numbers
if (a[key] /* || t[key] === 0 */) {
prev[key] = a[key]
}
return prev
}, {} as A)
Upvotes: 4