Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript dynamic object keys with defined values

I am encountering an issue trying to make typescript recognise the keys of a javascript object for me, while enforcing each key's value type because I want to create a typeof the keys of the object, so I can't just create a regular type MyObject = { [key: string]: <insert type> }.

Imagine an object myobject where I extract the keys of it like:

const myobject = {
  foo: {},
  bar: {}
};

type MyObjectKeys = keyof typeof myobject; // 'foo' | 'bar'

How can I add type definitions to the values of the keys, while still being able to extract/inherit the definitions of the keys? If I do something like this, then I will no longer be able to extract the exact keys of the object, but only the type (string):

type MyObject = { [key: string]: { value: boolean }}
const myobject = {
  foo: { value: true },
  bar: { value: false }
};

type MyObjectKeys = keyof typeof myobject; // string

I figured that I could achieve this by creating a helper function like:

function enforceObjectType<T extends MyObject>(o: T) {
    return Object.freeze(o);
}
const myobject = enforceObjectType({
  foo: {},
  bar: {}
});

But I'd prefer to define a clear type for it, without having to pollute the code, writing functions only related to types. Is there a way to allow a set of strings as keys of a type without repetition?

The purpose of this is to get TypeScript to help pointing out the right object keys like (the real usage is a bit more complex, so I hope this describes it well enough):

type MyObjectKeys = keyof typeof myobject; // string
function getMyObjectValue(key: MyObjectKeys) {
   const objectValue = myobject[key];
}

// suggest all available keys, while showing an error for unknown keys
getMyObjectValue('foo'); // success
getMyObjectValue('bar'); // success 
getMyObjectValue('unknown'); // failure

Wrap up: I want to define an object as const (in fact with Object.freeze) and be able to:

  1. Extract the exact keys of the object (without having to type a definition of each key).
  2. Define the type of each key, without overwriting the keys to string instead of what they are - like 'foo' | 'bar'.

Complete example

type GameObj = { skillLevel: EnumOfSkillLevels }; // ADD to each key.
const GAMES_OBJECT = Object.freeze({
   wow: { skillLevel: 'average' },
   csgo: { skillLevel 'good' }
)};

type GamesObjectKeys = keyof typeof GAMES_OBJECT;

function getSkillLevel(key: GamesObjectKeys) {
  return GAMES_OBJECT[key]
}

getSkillLevel('wow') // Get the actual wow object
getSkillLevel('unknown') // Get an error because the object does not contain this.

In accordance to above, I can't do the following because that will overwrite the known keys to just any string:

type GameObj = { [key: string]: skillLevel: EnumOfSkillLevels };
const GAMES_OBJECT: GameObj = Object.freeze({
   wow: { skillLevel: 'average' },
   csgo: { skillLevel 'good' }
)};

type GamesObjectKeys = keyof typeof GAMES_OBJECT;

function getSkillLevel(key: GamesObjectKeys) {
  return GAMES_OBJECT[key]
}

getSkillLevel('wow') // Does return wow object, but gives me no real-time TS help
getSkillLevel('unknown') // Does not give me a TS error

Another example: See this gist for example and copy it to typescript playground if you want to change the code

like image 508
Daniel Avatar asked Apr 03 '20 14:04

Daniel


People also ask

How do you define a object with key and value in TypeScript?

Use an index signature to define a key-value pair in TypeScript, e.g. const employee: { [key: string]: string | number } = {} . An index signature is used when we don't know all the names of a type's keys ahead of time, but we know the shape of their values. Copied!

How do you write an object with dynamic keys?

Use an index signature to define a type for an object with dynamic keys, e.g. [key: string]: string; . Index signatures are used when we don't know all of the names of a type's properties ahead of time, but know the shape of the values.

How do you get a dynamic object value in TypeScript?

To dynamically access an object's property: Use keyof typeof obj as the type of the dynamic key, e.g. type ObjectKey = keyof typeof obj; . Use bracket notation to access the object's property, e.g. obj[myVar] .

How do you define type of key in an object?

Use the keyof typeof syntax to create a type from an object's keys, e.g. type Keys = keyof typeof person . The keyof typeof syntax returns a type that represents all of the object's keys as strings. index.ts. Copied!


2 Answers

While I have not found a way to completely avoid creating a javascript function to solve this (also told that might not be possible at all, at this moment), I have found what I believe is an acceptable solution:

type GameInfo = { [key: string]: { skillLevel: 'good' | 'average' | 'bad' }}

type EnforceObjectType<T> = <V extends T>(v: V) => V;
const enforceObjectType: EnforceObjectType<GameInfo> = v => v;

const GAMES2 = enforceObjectType({
  CSGO: {
    skillLevel: 'good',
  },
  WOW: {
    skillLevel: 'average'
  },
  DOTA: {
    // Compile error - missing property skillLevel
  }
});

type GameKey2 = keyof typeof GAMES2;

function getGameInfo2(key: GameKey2) {
  return GAMES2[key];
}

getGameInfo2('WOW');
getGameInfo2('CSGO');
getGameInfo2('unknown') // COMPILE ERROR HERE

This way we get:

  1. Compile errors on missing properties.
  2. Autocomplete of missing properties.
  3. Able to extract the exact keys defined in the object, without having to define those elsewhere, e.g. in an enum (duplicating it).

I have updated my Gist to include this example and you might be able to see it in practice on typescript playground.

like image 146
Daniel Avatar answered Oct 09 '22 16:10

Daniel


Hope this help you now:

enum EnumOfSkillLevels {
  Average = 'average',
  Good = 'good'
}

type GameObject<T extends { [key: string]: {skillLevel: EnumOfSkillLevels} }> = {
  [key in keyof T ]: {skillLevel: EnumOfSkillLevels}
}
const GAMES_OBJECT = {
   wow: { skillLevel: EnumOfSkillLevels.Average },
  csgo: { skillLevel: EnumOfSkillLevels.Good },
   lol: { skillLevel: EnumOfSkillLevels.Good }
} as const;


function getSkillLevel(key: keyof GameObject<typeof GAMES_OBJECT>) {
  return GAMES_OBJECT[key]
}

getSkillLevel('wow') // Does return wow object, but gives me no real-time TS help
getSkillLevel('lol') // Does return wow object, but gives me no real-time TS help
getSkillLevel('unknown') // Does give me a TS error

Playground link.

like image 2
Pedro Mutter Avatar answered Oct 09 '22 15:10

Pedro Mutter