HanQ
HanQ

Reputation: 95

Type 'undefined' is not assignable to type 'never'

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

Answers (1)

ford04
ford04

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)

Playground

Upvotes: 4

Related Questions