0

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?

1 Answer 1

3

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

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for the answer! The reason I used keyof T is for vscode to correctly recognize the type and give hints when I type cls.data. Option 2 worked for me!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.