typescripttypescasting

Type 'string | boolean' is not assignable to type 'never' in typescript


Consider this class:

 const creditRiskRatingKeys = [
  'applicant_id',
  'is_dirty',
] as const;

const dict = {
  applicant_id: 'test_id',
  is_dirty: true,
}

class CreditRiskRating {
  applicant_id: string = '';
  is_dirty: boolean = false;

  fillQuestionDictionaryToModel() {
    for (const key of creditRiskRatingKeys) {
      this[key] = dict[key]; // Type 'string | boolean' is not assignable to type 'never'.
    }
  }
}

Key is already keyof CreditRiskRating and this is referring to the same class. Then why this[key] = dict[key]; should complain?? What am I missing here?

Playground


Solution

  • You've basically run into microsoft/TypeScript#32693 as well as microsoft/TypeScript#58905. See those issues for a canonical answer.


    An assignment of the form

    bar[k] = foo[k]
    

    where Pick<typeof foo, typeof k> is assignable to Pick<typeof bar, typeof k> (using the Pick utility type to say that we only care about the properties of foo and bar which exist at keys of type k) is almost certainly safe. But TypeScript fails to see this in most circumstances.

    The first problem happens when k is of a union type like "applicant_id" | "is_dirty". TypeScript only tracks the type of k, not its identity, so, for all TypeScript knows, you're writing

    bar[k2] = foo[k1]
    

    where k2 and k1 are both of the same union type. That wouldn't be safe, so TypeScript complains. The only way an assignment like that could be safe is if the foo[k1] has a type that works for all possible k2, which would be the intersection of the relevant property types, not the union:

    interface Foo {
        a: string,
        b: boolean,
    }
    interface Bar extends Foo {
        c: Date
    }
    function f(foo: Foo, bar: Bar, k: keyof Foo) {
        bar[k] = foo[k]; // error!
        //~~~~ <-- Type 'string | boolean' is not assignable to type 'never'.
    }
    

    This is the subject of microsoft/TypeScript#32693, and it's a longstanding open issue in TypeScript.

    TypeScript only wants to allow an assignment to bar[k] if that value is both a string and a boolean. There is no such value... in other words, string & boolean is the impossible never type. That's basically the error you're seeing.


    The recommended approach is to switch from union-typed keys to generic keys. So instead of k being of type MyKeys, you'd have it be of generic type K extends MyKeys. And if bar and foo are identical types, then that works:

    function g<K extends keyof Foo>(bar: Foo, foo: Foo, k: K) {
        bar[k] = foo[k]; // okay, because bar is a Foo
    }
    

    But if the two types are distinct, even if they are equivalent everywhere that matters, then you get an error again:

    function h<K extends keyof Foo>(bar: Bar, foo: Foo, k: K) {
        bar[k] = foo[k]; // error!
        //~~~~ <-- Property 'c' is missing in type 'Foo' but required in type 'Bar'.
    }
    

    This is the subject of microsoft/TypeScript#58905, and it's considered a design limitation of TypeScript. The workaround is to synthesize your own types for bar and foo such that they are seen as identical. This can happen by just widening them both to the equivalent of Pick types. Since every Bar is also a Foo, we can just widen bar and leave foo alone:

    function i<K extends keyof Foo>(bar: Bar, foo: Foo, k: K) {
        const _bar: Foo = bar; // okay
        _bar[k] = foo[k]; // okay
    }
    

    So, we need to make things generic, and the object types need to be identical. Doing both with your code looks like:

    type CreditRiskRatingKey = typeof creditRiskRatingKeys[number];
    
    class CreditRiskRating {
        applicant_id: string = '';
        is_dirty: boolean = false;
        fillQuestionDictionaryToModel() {
            const thiz: typeof dict = this;
            creditRiskRatingKeys.forEach(<K extends CreditRiskRatingKey>(key: K) => {
                thiz[key] = dict[key];
            });
        }
    
    }
    

    Here I changed your for...of loop to a generic callback to forEach(), and widened this to typeof dict before doing the assignment. Now it compiles without error. It's annoying to have to jump through such hoops, but it's the closest we can get to having TypeScript understand our logic. If you don't want to do that you can always use type assertions like this[key] = dict[key] as never.

    Playground link to code