Search code examples
typescriptmapped-types

Creating a mapped type from an object without errors in TypeScript


How does one create a mapped type from an object? If I attempt the following:

interface Stats {
  count: number,
  avg: number
}

class Blah<T> {
  private stats: { [K in keyof T]: Stats };

  constructor(foo: T) {
    this.stats = Object
      .keys(foo)
      .reduce((result, key) => Object.assign(result, { 
        [key]: { count: 0, avg: 1 } 
      }), {});
  }
}

...I get the error:

Type '{}' is not assignable to type '{ [K in keyof T]: Stats; }'

This appears to be because the initial value in reduce is does not match the interface.

I can change my declaration this as a workaround:

private stats: { [K in keyof T]?: Stats };

...but that now means that some values for a keyof T may be undefined according to the type.

How do you create a fully mapped type, given the inability to create a mapped object without intermediate results in JavaScript?


Solution

  • It's a bit tedious, but you can do the following:

    interface Stats {
      count: number,
      avg: number
    }
    
    type StatsMap<T> = { [K in keyof T]: Stats };
    
    class Blah<T> {
      public stats:StatsMap<T>;
    
      constructor(foo: T) {
        this.stats = Object
          .keys(foo)
          .reduce<StatsMap<T>>((result, key) => Object.assign(result, { 
            [key]: { count: 0, avg: 1 } 
          }), {} as any);
      }
    }
    
    const blah = new Blah({ a: 'a', b: 'b', c: 'c' });
    console.log(blah.stats.a.avg); // Yay, no error + autocomplete!
    

    The {} as any is required to tell TypeScript you know what you're doing :-/

    You can test this on the playground.

    Note that I made the stats memboer public to show the usage! This is not required or anything. You also do not have to create the StatsMap, but I find it easier to read compared to writing { [K in keyof T]: Stats } multiple times.