DEV Community

Cover image for Using keyof and typeof for types efficiently in TypeScript
Maya Shavin 🌷☕️🏡
Maya Shavin 🌷☕️🏡

Posted on • Originally published at mayashavin.com

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;
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The above equals the following declaration:

type UserKeys = "firstName" | "lastName"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  1. Extract a new type based on the keys of the kind ImageVariablesType, using keyof
   type ImageFields = keyof ImageVariablesType
Enter fullscreen mode Exit fullscreen mode

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

type ImageFields = keyof typeof ImageVariables
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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

type VariableValues = ImageVariablesType[ImageFields]
Enter fullscreen mode Exit fullscreen mode

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',
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

We can rewrite using EImageVariables as follows:

function transform(value: EImageVariables) {
    //perform something
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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];
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 👇🏼 😉

Top comments (1)

Collapse
 
shouldiuseavpn profile image
Should I use a VPN?

That was a fun read about the keyof and typeof operators and the deeper possible interplay between them. Cheers!

For anyone that wants a deeper dive into how to we could use just keyof with a exploratory walk with examples, I recommend my former coworker's recently updated post on TypeScript's keyof operator and its various applications with examples. It also shows the errors along the way of finding a better solution to a problem like in this post which was great, thanks.