Home > Enterprise >  Typescript object key as key of another object?
Typescript object key as key of another object?

Time:05-10

I'm creating a generic class like this:

type Config = {
  value: string
}
class MyClass<T> {
  configs:{[key in keyof T]: Config },
  data: {[key in keyof T]?: string}

  constructor(configs: { [key in keyof T]: Config }) {
    this.configs = configs;
    this.data= {};
  }

  init() {
    Object.entries(this.configs).forEach(([configName, config]) => {
      if (!(configName in this.data)) {
        this.data[configName] = dosomething(config); // dosomething returns string
      }
    });  
  }
}

Then use the class like:

const cls = MyClass({
  cfg1: {value: 'value1'},
  cfg2: {value: 'value2'},
})

cls.init();

And the cls.data is expected to be:

{
  cfg1: 'calculated1',
  cfg2: 'calculated2',
}

But in the forEach, configName is string and config is unknown.

I tried ([configName, config]: [key in keyof T, Config]) => but that's not working.

How can I do this?

CodePudding user response:

Well the function Object.entries is not able to correctly infer the types here. When we look at the type definition of this function it looks like this:

entries<T>(o: { [s: string]: T } | ArrayLike<T>): [string, T][];

But you are passing an object of type { [key in keyof T]: Config } to the function. TypeScript does not see those as comparable and so the type [string, unknown][] is inferred.


So how can you solve this?

Option 1

The easiest way would be to change { [key in keyof T]: Config } to { [key : string]: Config } everywhere.

configs:{[key : string]: Config }
data: {[key : string]: string}

constructor(configs: { [key : string]: Config }) {
  this.configs = configs;
  this.data= {};
}

This is essentially identically to the code you had before. You didn't restrain the type T to have any particular shape so the keys of T could have been any string value anyway. In fact, since we don't use T anymore the class does not have to be generic.

Let me know if this works for your specific use case. Or if using key in keyof T makes a difference for you.


Option 2

The second option would be to set a constraint on T:

class MyClass2<T extends Record<string, any>> { /* ... */ }

Now TypeScript knows that keyof T produces string values and the correct types are inferred.

But now we get another error:

this.data[configName] = dosomething(config)
// Type 'string' cannot be used to index type '{ [key in keyof T]?: string | undefined; }'

This is because Object.entries will always return string as the first type in the tuple because it is hard-coded in the type definition.

To fix this we have (sadly) to give TypeScript more information:

this.data[configName as keyof T] = dosomething(config);

Playground

  • Related