Skip to main content
TypeScript keyof with Examples
11 min read

TypeScript keyof with Examples

Introduction

The keyof operator in TypeScript is used to derive new types from an existing object type's keys. It is a TypeScript construct commonly used as a building block in generating advaced types from a source object type.

TypeScript keyof is a trivial type manipulation operator introduced in v 2.1. It creates a union of string and numerical literal types from the keys of the source object type. TypeScript keyof typically works in conjunction with other operators such as extends, typeof, in, as and constructs like index signature syntax to define sophisticated types that often involve properties of important data entities in an application.

In this post, we learn how the TypeScript keyof operator works and explore its use cases in multiple scenarios. We first encounter a common example that involves using the keyof operator with the Object.keys() method. We also cover an example of TypeScript generic functions that ensures type safety using keyof with extends.

In the later part of the post, with a couple of mapped type examples using index signature syntax, we demonstrate how keyof works with typeof, in and as operators in composing mapped types of different complexity levels. Towards the end, we also show an use case of keyof with common TypeScript utility types such as Omit<> and Exclude<>.

Steps we will cover in this post:

Prerequisites

In order to properly follow this post and test out the examples, you need to have a JavaScript engine and you should have knowledge about at least the basics of TypeScript and utility types.

TypeScript Setup

Your JavaScript engine has to have TypeScript installed. It could be Node.js in your local machine with TypeScript supported or you could use the TypeScript Playground.

What is TypeScript keyof ?

TypeScript keyof is an object type operator which generates a union type of string and numerical literal types from the keys of an existing object type. It is passed the reference type from which the union of keys are generated. The syntax looks like this:

type TUnionType = keyof ExistingObjectType;

How TypeScript keyof Works

TypeScript keyof takes a passed reference object type and creates a union from its keys. We can alias the resulting union as a type. An example looks like this:

type TAccount = {
username: string;
email: string;
password: string;
role: string;
};

type TAccountKeys = keyof TAccount; // Equivalent to: "username" | "email" | "password" | "role"

Notice, the keys above in TAccount type get coerced to strings in the union. If we had any key that were a number, they would remain a numeric literal:

type TAccount = {
username: string;
email: string;
password: string;
4: string;
};

type TAccountKeys = keyof TAccount; // Equivalent to: "username" | "email" | "password" | 4

TypeScript keyof: Object Type Keys vs Object Keys

It is important to note that TypeScript keyof creates the union from a reference object type, not from the object itself. So, the distinction here is keyof always has to be passed the object type as argument, not the actual object.

Because of this, we should first have the object type that represents an object or application entity so that we can pass it to keyof. It can be a directly described object type, as in the above example. Or it can be an object literal type derived with the typeof operator as below:

const account = {
username: "",
email: "",
password: "",
role: "",
};

type TAccount = typeof account; // { username: string; email: string; password: string; role: string; }

type TAccountKeys = keyof TAccount; // Explicitly: "username" | "email" | "password" | "role"

TypeScript keyof typeof Pair: An Object.keys() Iteration Example

It is common to use the typeof operator with keyof in order to derive an object type first. One use case is while iterating over an object with its keys extracted with Object.keys():

const account = {
username: "donald_duck",
email: "donald.duck@exmaple.com",
password: "goawaygoaway",
role: "admin",
};

const accountKeys = Object.keys(account);

const text = accountKeys?.map(
(key) => `Hello, your ${key} is ${account[key as keyof typeof account]}`,
);
console.log(text);

Here, we are using the keyof operator with typeof. typeof first gives us the literal object type from account which we use as keyof's type argument. And then we use the created union type to assert the key with as to be an item in the union.

Without the type assertion used above for the keys of account object mapped, we get an implicit any error:

const text = accountKeys?.map((key) => `Hello, your ${key} is ${account[key]}`); // `account[key]` Gives the following 7053 error:
/*
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ username: string; email: string; password: string; role: string; }'.
No index signature with a parameter of type 'string' was found on type '{ username: string; email: string; password: string; role: string; }'.(7053)
*/

This happens because TypeScript is unable to relate the inferred string type for the keys in accountKeys to the object literal keys in the account object. So, we have to resort to type assertion with as and set the key's type to a derived union of strings when key's context is the account object.

This is pretty handy in cases where the literal object type does not need to be aliased for repeated use.

TypeScript keyof: Advanced Use Cases

There are numerous use cases where TypeScript keyof operator is utilized. In most cases, the keyof operator is used to ensure type safety through compatibility, and type specificity through narrowing and constraining. In the sections that follow, we elaborate the most common examples.

Using TypeScript keyof in Generic Types

We can use the keyof operator in generic functions. Let's say we have an account object with a few properties. We make use of the keyof operator to attain type compatibility in generic getter and setter methods, such as in the following code snippet:

const account = {
username: "donald_duck",
email: "donald.duck@exmaple.com",
password: "goawaygoaway",
role: "admin",
};

function getProp<T, K extends keyof T> (obj: T, prop: K) { return obj[prop] };
function setProp<T, K extends keyof T> (obj: T, prop: K, value: T[K]) { obj[prop] = value };

console.log(getProp(account, "email")); // "donald.duck@exmaple.com"
console.log(getProp<{"name": string}, "name">(account, "email")); // 2345 Error: "name" not compatible to `account` keys

setProp(account, "role", "project manager");
setProp<{"name": string}, "name">(account, "role", "project manager")> // 2345 Error

In this example, we have defined a couple of functions that take an object as argument. We are attempting to achieve type safety by assigning generic type parameters to these methods. With T, K extends keyof T generic annotation, we are declaring whatever the type of the first type parameter is (T) is we want the second parameter (K) to be only among its keys. We then assign these type params to the functions arguments accordingly, basically saying that the first argument obj should be of type T, the second argument of K and the third one T[K].

This makes the call to getProp and setProp without type params to infer the type from the passed in account object. However, when we pass an inconsistent type param to either, TypeScript reminds with a 2345 error that the types and object passed are incompatible.

So, keyof along with extends in this example plays an important part in achieving type safety through compatibility in generic functions.

TypeScript keyof with Generic Type Mappers

The TS keyof operator is one of the building blocks that help derive complex mapped types from a source object type. TypeScript mapped types build on top of index signature syntax to compose a new type with the object type's keys extracted with the keyof operator.

Below is an example of generating a mapped type from an existing TAccount type using a custom defined generic type mapper utility that internally uses keyof:

type TAccount = {
username: string;
email: string;
password: string;
role: string;
};

type TEntityPropsMapper<T> = {
[Property in keyof T]: {
protectedField: boolean;
description: string;
};
};

type TAccountProps = TEntityPropsMapper<TAccount>;
/*
{
username: {
protectedField: boolean;
description: string;
};
email: {
protectedField: boolean;
description: string;
};
password: {
protectedField: boolean;
description: string;
};
role: {
protectedField: boolean;
description: string;
};
}
*/

In this example, the TEntityPropsMapper<T> utility is a custom defined generic type mapper utility that takes the entity type (T) as argument. Internally, it generates the union of T's keys with keyof T. It then uses the in narrowing operator to loop through the union members to create a new type by applying the index signature syntax. The keyof operator restricts the keys to union members. The derived type has a new shape in its values.

Using keyof like this in a custom type mapper utility helps achieve type specificity through narrowing and union constraints.

TypeScript keyof: A Remapped Type Example

Let's now look at a modified version of the above example. We'd like to generate a remapped type that uses the as operator:

type TAccount = {
username: string;
email: string;
password: string;
role: string;
};

type TEntityPropGetterMapper<T> = {
[Property in keyof T as `get${Capitalize<
string & Property
>}`]: () => T[Property];
};

type TAccountProps = TEntityPropGetterMapper<TAccount>;
/*
{
getUsername: () => string;
getEmail: () => string;
getPassword: () => string;
getRole: () => string;
}
*/

This time, notice that we are applying a remapping of the Property name to its getter method with get${Capitalize<string & Property>}(). We are basically mapping each property key to give its respective getter method a new name. In a remapped type also, keyof plays a crucial role by constraining the keys to the generated union type.

TypeScript keyof with Utility Types

We can use the keyof operator with regular TypeScript transformation utilities.

For example, in a scenario where we'd like to hide some fields from the TAccount type, we'd derive a new type from it for that purpose. Let's say, we hide the password and role fields and have an object for it. And then we derive another type with exposed properties. We can use the Omit<> transformation utility for this:

type TAccount = {
username: string;
email: string;
password: string;
role: string;
};

const hiddenFields = {
password: "",
role: "",
};

type TExposedAccountInfo = Omit<TAccount, keyof typeof hiddenFields>;
/*
{
username: string;
email: string;
}
*/

type TAccountLoginOptions = keyof TExposedAccountInfo; // "username" | "email"

In this example, we have used the keyof operator for passing the exact keys to be omitted. And they are those in the type for hiddenFields.

Notice the returned type is an object type. We have made the returned type an union with another keyof in TAccountLoginOptions.

The same union in the last TAccountLoginOptions type can be obtained with the Exclude<> utility type:

type TAccountLoginOptions = Exclude<keyof TAccount, keyof typeof hiddenFields>;

The difference between them is Omit<> works on object types. And in contrast Exclude<> is used only on union of types.

Summary

In this post, we explored with examples the TypeScript keyof operator. We learned how it derives union of string and numerical literal types from the keys of an existing object type. We elaborated with a series of examples that keyof play important role in providing type safety through enforcing compatibility and type specificity through narrowing and constraints.

We covered the use of keyof in iterating an object with its Object.keys(). We saw examples of generic functions and type mapper utilities that uses keyof operator under the hood. We also explored how to use keyof operator with TypeScript transformation utilities such as Omit<> and Exclude<>.