How to define an object type with dynamic keys in TypeScript?

Viewed 8621

I have a const object which looks like this:

export const Language: Values = {
  es: {urlValue: 'es', label: 'Spain'},
  en: {urlValue: 'en', label: 'Anything'},
  eb: {urlValue: 'eb', label: 'Other thing'},
};

export interface Values {
  [name: string]: Value;
}

export interface Value {
  urlValue;
  label;
  selected?;
}

Now somewhere in app, we read some data which has this format, which coincidentally has the exact same keys:

{
  es: 32,
  en: 11,
  eb: 56
}

So actually i need another object type like this:

export class AnotherObject {

  constructor(public es: number,
              public en: number,
              public eb: number) {
  }
}

But can i create this type also dynamically so that it automatically uses the keys of the Language object, or urlValue of "Value" type?

PS: The question is a simplified form of our exact use case, where we have multiple consts like Languages. So an automatism would be really helpful.

Edit: For the suggestion of @basarat - Since this object is a part of a parent object i actually need a type definiton in this part:

export class ParentObject {
  thatObjectAbove: AnotherObject;
  otherObjects: ..
  ..
}

export interface AnotherObject {
  [name: LanguageOptions]: number;
}

I have the error "An index signature parameter type cannot be a union type. Consider using a mapped object type instead." on the 'name' part of line "[name: LanguageOptions]: number;"

3 Answers

The keyof type operator gives you the key names as a type:

export const Language = {
  es: {urlValue: 'es', label: 'Spain'},
  en: {urlValue: 'en', label: 'Anything'},
  eb: {urlValue: 'eb', label: 'Other thing'},
};

type LanguageOptions = keyof typeof Language; // 'es' | 'en' | 'eb'

Continuing this answer:

I have achieved my goal with defining the AnotherObject as type rather than interface.

type LanguageOptions = keyof typeof Language;  // as from basarat's answer

type AnotherObject = {[key in LanguageOptions]: number}; // is actually a type, still is named as 'object' so that it is still compatible with the question's code
    
export class ParentObject {
  thatObjectAbove: AnotherObject;
  otherObjects: ..
  ..
}

As it is stated here, it is because of the strange behaviour of typescript, which can be fixed in this way.

So the end solution in one-liner:

thatObjectAbove: {[key in keyof typeof Language]: number};

or

thatObjectAbove: Record<keyof typeof Language, number>;  // Record is a built-in typescript type

Would you might to change your code a bit?

class DynamicClass<T> {
    constructor(values?: { [key in keyof T]?: T[key] }) {
        for (const key of Object.keys(values)) {
            this[key] = values[key];
        }
    }
}

class MyClass extends DynamicClass<MyClass> {
    attr1: string;
    attB: number;
    obj: {
        wow: string,
        yeah: boolean
    };
}


new MyClass({
    attr1: 'a',
    obj: {
        wow: '',
        yeah: true
    },
    att
})

enter image description here

Related