9

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

6
  • I didn't understand it very well... Do you want a function that receives as parameters the keys of any given object? Or you want to set myObject as constant? Commented Apr 3, 2020 at 14:46
  • I'm sorry about that. I will try to edit and express it better, but I basically just want to define the type of each key of myobject while still being able to "inherit" the keys dynamically. Commented Apr 3, 2020 at 15:08
  • Np, we are here to help you. Could you please post a complete example, of exactly what you want to happen? Commented Apr 3, 2020 at 15:10
  • Ok I have updated the question to include a more clear example of what I want to achieve. Let me know if I should create some sort of link to a playground for this. Commented Apr 3, 2020 at 15:19
  • So you want to "extend" the GameObj and the getSkillLevel to keeps just accepting the right keys, not just string, right? Commented Apr 3, 2020 at 15:29

4 Answers 4

6

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.

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

Comments

3

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.

11 Comments

Thanks for your response. While I am totally aware of this solution, it is not the one I seek, as I have to create duplicates of each key. That is redundant and annoying if you have a long list of keys.
Is GAMES_OBJECT a fixed constant that you change manually, or it is a base object to create another?
No, I mean the new link I commented above. I removed the games enum
Ok, I see, but the getSkillLevel receives a param of type GameObject<typeof GAMES_OBJECT> and GameObject type needs a generic which extends the type you want, extends { [key: string]: {skillLevel: EnumOfSkillLevels}. So TS is gonna complain on the function, not in the object itself. It is a problem for you?
|
1

I write my codes in this way:

function define<T>(o:T){
  return o;
}
type Node = {
  value: number,
  key: string
}
const NODE_DIC = {
  LEFT: define<Node>({
    value: 1,
    key: '1'
  }),
  RIGHT: define<Node>({
    value: 2,
    // compile error for missing `key`
  })
}

Here is the type of the object:

{
    LEFT: Node;
    RIGHT: Node;
}

1 Comment

Why not just: LEFT: {...} as Node ?
0

Since TypeScript 4.9, the satisfies operator can be used to verify that an expression matches a specific type without changing the type of that expression.

const GAMES = Object.freeze({
  CSGO: {
    skillLevel: 'good'
  },
  WOW: {
    skillLevel: 'average'
  }
}) satisfies GameInfo;

Comments

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.