Home > Software engineering >  How to tell typescript the correct indexed key
How to tell typescript the correct indexed key

Time:11-18

    type Type = {
      aa: string;
      bb: number;
    };
    
    const keys = ['aa', 'bb'] as (keyof Type)[];
    const target = {} as {
      [Key in keyof Type]: Type[Key];
    };
    
    const source: Type = {
      aa: 'aa',
      bb: 123
    };
    keys.forEach((key) => {
      const s = source[key]; //string | number
      const t = target[key]; //string | number
      /**
       * Error: Type 'string | number' is not assignable to type 'never'.
       * Type 'string' is not assignable to type 'never'.
       */
      target[key] = source[key];
    });

As the code show above target[key] = source[key]; is not allowed, here is my reason about:

source[key] is string | number and target[key] is also string | number, so the assignment is not allowed, but the compiler ignore the fact that key is actually the same one, i.e. if source[key] is string, target[key] must be string as well,

How do I tell the typescript about the constraint, it seem that I should put some generic type param somewhere, but no idea what exactly I should do

CodePudding user response:

As you note, the following doesn't work

keys.forEach(key =>
  target[key] = source[key] // error! string | number not assignable to never
);

because the compiler is not keeping track of the correlation between the union types of target[key] and source[key]. They are both string | number, but the compiler does not realize that they will either both be string or both be number. It does not notice that the identity of key ties the two sides of the assignment together.

Since indexed access types were made more strict in TypeScript 3.5, as implemented in microsoft/TypeScript#30769, the compiler will only allow assignments like target[key] = source[key] for union-typed key if every possible assignment would succeed, by treating the assignment target as the intersection of the property types. That is, target[key] would only accept a value of type string & number (a.k.a. the impossible never type) instead of string | number.


Lack of direct support for correlated unions is reported at microsoft/TypeScript#30581.
And the recommended fix, as described in microsoft/TypeScript#47109, is to use generics. Essentially, when key is generic and when target and source have the same type, then the compiler will allow target[key] = source[key] (even though this is unsound, as mentioned in this comment in microsoft/TypeScript#30769).

So to fix your example, we should make the forEach() callback a generic function, like this:

keys.forEach(<K extends keyof Type>(key: K) => {
  target[key] = source[key]; // okay
});

Now key is of generic type K constrained to keyof Type. Both sides of the assignment are of identical generic type Type[K], and the compiler allows this.

Playground link to code

  • Related