Using keyof and typeof for types efficiently in TypeScript

Feb 15, 2023 ยท 7 min read
Using keyof and typeof for types efficiently in TypeScript

In this article, we will learn how to extract and declare constant types by combining the type operators typeof, keyof and enums to benefit your codebase.

Prerequisites

For a better coding experience, you should install TypeScript in your IDE, such as VSCode. It will provide you with a lot of essential features such as error highlighting, IntelliSense, linting, etc... You should also install some extensions such as JavaScript and TypeScript Nightly, ESLint, etc.

Understanding typeof

In TypeScript, we can use typeof to extract the type of a variable or a property, as shown in the below example:

const Name = {
    firstName: 'Maya',
    lastName: 'Shavin'
};

let myName: typeof Name;

In the above code, we declare the myName variable with the type extracted from the variable Name, which will be an object with two properties - firstName and lastName. When hovering on myName in your IDE, TypeScript will show you the type for myName concluded from Name as in the following screenshot:

A screenshot displaying the type Name with first name and last name as its properties

Note here with typeof, if the variable to extract type is an Object, the type received will be a complete type structure, and each property has its type (such as type of myName will have two fields - firstName of type string, and lastName of kind string).

Another valuable scenario for typeof is combining it with ReturnType to extract the type of the returned data from a function. To do so, we perform the following:

  1. Get the function type of the declared function using typeof
  2. Use ReturnType<T> with T is the extracted type in step 1 to get the type of the return value from T.

The following example demonstrates how we can extract the return type of decoupleName()

function decoupleName (name: string) {
    const [firstName, ...remaining] = name.split(" ");

    return {
        firstName,
        lastName: remaining.reduce((text, word) => text ? `${text} ${word}` : word, '')
    }
}

type NameObj = ReturnType<typeof decoupleName>

TypeScript will automatically refer to the correct type of NameObj, shown in the screenshot below:

A screenshot displaying the type NameObj with first name and last name as its properties

Additionally, we can use typeof in TypeScript to act as the type guard within a conditional block, just like in JavaScript, though in this case, it will mainly work for primitive types like string, object, function, etc...

Now we understand typeof and its usage. Next, we will explore keyof.

Understanding keyof

While typeof produces the type represented by a variable, keyof takes an object type and produces a type that is the liberal union of that variable's keys, as seen below:

interface User {
    firstName: string;
    lastName: string;
};

type UserKeys = keyof User

The above equals the following declaration:

type UserKeys = "firstName" | "lastName"

However, unlike typeof, you can't use keyof directly on a variable. TypeScript will throw an error for the following code:

const Name = {
    firstName: 'Maya',
    lastName: 'Shavin'
};

type NameKeys = keyof Name; //error

To extract types from object variables, such as an object map of constant values, we need to combine keyof and typeof, which we will learn next.

Extract type from an object's keys (or properties)

Take the following ImageVariables, which acts as a map for variables used in modifying an image, for instance:

export const ImageVariables = {
  width: 'w',
  height: 'h',
  aspectRatio: 'ar',
  rotate: 'a',
  opacity: 'o',
} as const;

Note here we need the const at the end of the object to indicate it as read-only. Otherwise, TypeScript won't allow us to extract the types from it due to the risk of modifying the object's internal properties on the go.

ImageVariables contains the mapping from its keys to matching variable symbols used in transforming an image according to the Cloudinary mechanism. To generate a type based on the ImageVariables's properties (or keys), we perform the following:

  1. Get the type representing ImageVariables using typeof
    type ImageVariablesType = typeof ImageVariables
    
  2. Extract a new type based on the keys of the kind ImageVariablesType, using keyof
    type ImageFields = keyof ImageVariablesType
    

Alternatively, we can combine the two steps above into one, as follows:

type ImageFields = keyof typeof ImageVariables

That's it. We now have ImageFields type, which contains the accepted fields for ImageVariables, like in the screenshot below:

A screenshot displaying the type ImageFields with all the keys of ImageVariables as its acceptable values

We can now use this generated type as follows:

const transformImage = (field: ImageFields) => {
    const variable = ImageVariables[field]

    //do something with it
}

By declaring the type based on properties of ImageVariables, the flow for any usage of transformImage is secure, and we can ensure that the field passed will always need to exist within ImageVariables. Otherwise, TypeScript will detect and alert the user of any error.

A screenshot displaying error by TypeScript when passing a non-exist value in ImageFields to the function transformImage

Also, Intellisense will inform the user of the acceptable values, limiting the possibility of passing the wrong value.

A screenshot displaying a hint dropdown for accepted values to pass to transformImage

On a side note, the type-check behaves similarly to using the hasOwnProperty() check in runtime, though it only happens in compile time.

Sound straightforward enough. What if we want to extract the values of the keys from ImageVariables into a new type? Let's look at it next.

Extract type from values of object's keys (or properties)

If we want to generate a type from the values of the keys of ImageVariables, we can do the following:

type VariableValues = typeof ImageVariables[ImageFields]

Since we already declare ImageVariablesType as type of ImageVariables, we can rewrite the above as:

type VariableValues = ImageVariablesType[ImageFields]

With the above code, we now have a new type, VariableValues which accepts the following values:

A screenshot displaying the type generated from values of ImageVariables' keys

Generating type from values and keys of a named constant object is advantageous in many scenarios, such as when you have to work with various data mappings, and standard keys or values are mapping between them. Using keyof and typeof on the object mapping can help create the connection between related maps and types, thus avoiding potential bugs.

Alternatively, we can combine enums and typeof to achieve the same goal.

Bonus: working with enums

Enums are a convenient and organized way to declare named constant types. It allows us to create a set of distinct constant values, and each enum field is either numeric or string-based. We can rewrite our ImageVariables as follows:

enum EImageVariables {
  width = 'w',
  height = 'h',
  aspectRatio = 'ar',
  rotate = 'a',
  opacity = 'o',
}

One advantage of using an enum is we can use the name of the enum as the type for accepted values declaration. Hence, instead of the following code:

type VariableValues = typeof ImageVariables[ImageFields]

function transform(value: VariableValues) {
    //perform something
}

We can rewrite using EImageVariables as follows:

function transform(value: EImageVariables) {
    //perform something
}

The type-check performs the same for less code ๐Ÿ˜‰. Nevertheless, for getting the type from the keys (or properties) of the declared enum EImageVariables, we still need to use the combination keyof typeof like for a regular constant object:

type ImageFields = keyof typeof EImageVariables

That's it. The above code yields the same result as when we use ImageVariables.

Now let's take a recap of how we get the type from keys and values of a constant object:

export const ImageVariables = {
 width: 'w',
 height: 'h',
 aspectRatio: 'ar',
 rotate: 'a',
 opacity: 'o',
} as const;

type ImageVariablesType = typeof ImageVariables;
type ImageFields = keyof ImageVariablesType;
type VariableValues = ImageVariablesType[ImageFields];

In comparison with using an enum:

enum EImageVariables {
  width = 'w',
  height = 'h',
  aspectRatio = 'ar',
  rotate = 'a',
  opacity = 'o',
}

type EImageVariablesType = typeof EImageVariables;
type EImageFields = keyof EImageVariablesType;
//No need for getting the type of values

And same as constant objects, we can use the values of the enum's keys directly, such as EImageVariables.width in our code. In the runtime, enums exist in your compiled code as JavaScript Objects and behave like one.

In general, enums are okay. In TypeScript, due to the non-efficient way it was implemented (hopefully, this has been fixed or will be short), many argue its impact on performance.

So should we use them? It depends. The choice is up to you.

Summary

We often ignore the type operators like typeof or keyof when using TypeScript since they are too primary. However, they play an essential role in building your type system. They can help develop advanced and complex types for your application when combined correctly and with other TypeScript syntax. Let's give them a try and see what they can bring you.

๐Ÿ‘‰ If you'd like to catch up with me sometimes, follow me on Twitter | Facebook.

Like this post or find it helpful? Share it ๐Ÿ‘‡๐Ÿผ ๐Ÿ˜‰