Search code examples
typescripttypescript-genericstypescript-eslint

Type 'string | number' is not assignable to type 'never' in Typescript


When I write some code, I have some problems like that:


function getObjectKeys<T extends object>(object: T) {
    return Object.keys(object) as (keyof T)[]
}

const props = {
    propA: 100,
    propB: 'text'
}

const store = { ...props }

getObjectKeys(props).forEach((key) => {
    store[key] = props[key]
})

reported some errors:

const store: {
    propA: number;
    propB: string;
}
Type 'string | number' is not assignable to type 'never'.
  Type 'string' is not assignable to type 'never'.

when I write like this:


getObjectKeys(props).forEach((key) => {
    if (key === 'propA') {
        store[key] = props[key]
    } else if (key === 'propB'){
        store[key] = props[key]
    } else {
        store[key] = props[key]
    }
})

It can work but not so good. how to solve them?


Solution

  • The underlying issue is a type safety improvement implemented by microsoft/TypeScript#30769 when doing assignments to unions of object properties. When faced with code like

    type PropKeys = keyof typeof props; // "propA" | "propB"
    getObjectKeys(props).forEach((key: PropKeys) => {    
        store[key] = ... // what is allowable here
    });
    

    the compiler sees that key is of the union type "propA" | "propB". It doesn't know whether key is "propA" or "propB", so to be safe, it only wants to allow assignments which would work no matter which it turns out to be. That means the intersection of the property types. Since these types are number and string, the intersection is number & string, which reduces to the impossible never type because no values are of both types. And so the compiler cannot allow any assignment to store[key].

    The fix in this case is to replace values of union types with values of generic types that are constrained to the union. When the key or object is generic, the safety check from microsoft/TypeScript#30769 is not applied. This is potentially unsafe, but it is allowed, as mentioned in this comment. It looks like this:

    getObjectKeys(props).forEach(<K extends PropKeys>(key: K) => {
      store[key] = props[key]; // okay
    })
    

    That compiles with no error, and is as close to type safe as you can get. If you change the assignment to something definitely unsafe, like copying props.propA into store[key], you will get a warning again:

    getObjectKeys(props).forEach(<K extends PropKeys>(key: K) => {  
      store[key] = props.propA; // error!
    })
    

    Playground link to code