7-Dec

JavaScript

A smarter way of using T[keyof T]

In this article I will show how you can infer the value of a property in any object with generic typescript.

This articles assumes you are already familiar with generics in typescript.

2 min read

·

By Miina Lervik

·

December 7, 2022

Working with generic values in typescript can be difficult. Especially when we want to create constraints where one value knows something about another value. In this example we will look at how we can connect a property key with its value type over two different generic values.

Let us start with the following definition for an object. This will be the object we can test our code with:

  type UserType = {
    name: string;
    age: number,
    isAdmin: boolean;
  }

The wrong way

We now want to create a type that lets us define the keys of our object and it's values. We could do something like this:

  type KeyValueType<T extends Record<string, unknown>, K extends keyof T> = {
    key: K;
    value: T[K];
  }

But it will not work exactly how we like it to work. Although key is defined as a key from our object (K extends keyof T), it isn't able to connect that the key "name" should have the value string. Instead it assumes that value must be one of the possible values in our object. In this case it could be either string, number or boolean.

  // invalid but it gives no error
  const obj1: KeyValueType<UserType, keyof UserType> = {
    key: "name", // (property) key: keyof UserType
    value: false // (property) value: string | number | boolean
  }

What we want is to create a type that will give us an error if value has the wrong type for the given property name.

The right way

Instead of using <T, K extends keyof T>, we start by creating a type that lets us get all the values of an object.

  type ValueOf<T> = T[keyof T];

If we call ValueOf<UserType> it will give us string | number | boolean.

We then use ValueOf around the object type we want to define.

  type KeyValueType2<T extends Record<string, unknown>> = ValueOf<{
    [K in keyof T]: {
      key: K;
      value: T[K];
    }
  }>


  // Gives the following error: 
  //   Types of property 'key' are incompatible.
  //     Type '"name"' is not assignable to type '"isAdmin"'.
  
  const obj2: KeyValueType2<UserType> = {
    key: "name", // (property) key: "name"
    value: false // (property) value: string
  }

Now it works! It does give us an error if we try to set value to anything other than a string.

Let us take a closer look at this type to understand how it works. If we where to use it with our UserType we would get this:

type UserValueType = KeyValueType2<UserType>;

// hover over UserValueType to see what it looks like:
type UserValueType = {
    key: "name";
    value: string;
} | {
    key: "age";
    value: number;
} | {
    key: "isAdmin";
    value: boolean;
}

We have created one definition for each of the properties in UserType. This way we have created a connection between e.g the key "name" and its correct value type string.

Up next...

Loading…