Yan Sun I am a full-stack developer. Love coding, learning, and writing.

How to use the keyof operator in TypeScript

7 min read 2149

In JavaScript, we often use Object.keys to get a list of property keys. In the TypeScript world, the equivalent concept is the keyof operator. Although they are similar, keyof only works on the type level and returns a literal union type, while Object.keys returns values.

Introduced in TypeScript 2.1, the keyof operator is used so frequently that it has become a building block for advanced typing in TypeScript. In this article, we’re going to examine the keyof operator and how it is commonly used together with other TypeScript features to achieve better type safety with TypeScript generics, TypeScript mapped types, and TypeScript string literal types.

Let’s look at how each one interacts with the keyof operator.

Jump ahead:

Defining the keyof operator

The TypeScript handbook documentation says:

The keyof operator takes an object type and produces a string or numeric literal union of its keys.

A simple usage is shown below. We apply the keyof operator to the Staff type, and we get a staffKeys type in return, which represents all the property names. The result is a union of string literal types: “name” | “salary“:

type Staff = {
 name: string;
 salary: number;
 } 
type staffKeys = keyof Staff; // "name" | "salary"

In the above example, the keyof operator is used for an object type. It can also be used for non-object types, including primitive types. Below are a few examples:

type BooleanKeys = keyof boolean; // "valueOf"

type NumberKeys = keyof number; // "toString" | "valueOf" | "toFixed" | "toExponential" | "toPrecision" | "toLocaleString"

type SymbolKeys = keyof symbol; 
//typeof Symbol.toPrimitive | typeof Symbol.toStringTag | "toString" | "valueOf"

As you can see, it’s less useful when applied to primitive types.

Object.keys vs. keyof operator

In JavaScript, Object.keys are used to return an array of keys of the Object. In the below code, the returned keys are used to access the value of each property:

const user = {
  name: 'John',
  age: 32
};

console.log(Object.keys(user));
// output: Array ["name", "age"]
Object.keys(user).forEach(key => {
  console.log(user[key])
}) // output: John, 32

It’s worth noting that Object.keys ignore symbol properties in JavaScript. To overcome this issue you can use Object.getOwnPropertySymbols which returns an array comprised of only symbol keys.

Object.keys works similarly in TypeScript. Below is the TypeScript declaration of Object.keys:

interface ObjectConstructor {
 //...
 keys(o: object): string[]
 //...
}

If we run the earlier code snippet in TypeScript, we get the same output for Object.keys:

const user = {
  name: 'John',
  age: 32
};

But, when we iterate the keys and access the object property by the key, TypeScript throws an error. Please note that the error occurs only when the TypeScript Strict mode is turned on:

Object.keys(user).forEach(key => {
  console.log(user[key]) // error is shown
})

TypeScript Object.keys Error

The error is because we try to use the string type key to access the object with union type “name” | “age“.

You might be wondering why TypeScript doesn’t return typed keys as “name” | “age“?

This is intentional. Anders Hejlsberg explains the reason in this comment.

In a nutshell, the strongly typed Object.keys are fine at compile time. But objects often have extra properties at runtime. If this is the case, Object.keys will return extra keys, those extra keys will violate the assumption that keyof is an exhaustive list of the key of the object. This may cause the app to crash. I created a StackBlitz example to demonstrate this behavior.

To workaround this restriction, the simplest solution is to use type assertion with the keyof operator:

type userKeyType = keyof typeof user; //  "name" | "age"
Object.keys(user).forEach((key) => {
 console.log(user[key as userKeyType])
})

A more elegant solution is to extend the ObjectConstructor interface by declaration merge:

interface ObjectConstructor {
  keys<T>(o: T): (keyof T)[];
}

Object.keys(user).forEach((key) => {
  console.log(user[key]);
});

Please note that both workarounds will have the same restriction described in Anders’s comment.

So please use these workarounds with caution. You only want to use these methods when you are sure that there won’t be additional properties added to your object at runtime; otherwise, it may cause an unexpected crash.

Using keyof with TypeScript generics

The keyof operator can be used to apply constraints in a generic function.



The following function can retrieve the type of an object property using generics, an indexed access type, and the keyof operator:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

If you are new to TypeScript, this may look a little complex. Let’s break it down:

  • keyof T returns a union of string literal types. The extends keyword is used to apply constraints to K, so that K is one of the string literal types only
  • extends means “is assignable” instead of “inherits”; K extends keyof T means that any value of type K can be assigned to the string literal union types
  • The indexed access operator obj[key] returns the same type that the property has

We can see how the getProperty type is used below:

const developer: Staff = {
  name: 'Tobias',
  salary: 100, 
};

const nameType = getProperty(developer, 'name'); // string 
// Compiler error 
const salaryType getProperty(developer, ‘pay’); //Cannot find name 'pay'.(2304)

The compiler will validate the key to match one of the property names of type T because we apply the type constraint for the second parameter. In the above example, the compiler shows the error when an invalid key '``pay``' is passed.

If we don’t use the keyof operator, we can declare a union type manually:

type staffKeys = 'name' | 'salary';
function getProperty<T, K extends staffKeys>(obj: T, key: K): T[K] {
return obj[key];
}

The same type of constraint is applied, but the manual approach is less maintainable. Unlike the keyof operator approach, the type definition is duplicated and the change of the original Staff type won’t be automatically propagated.

Using keyof with TypeScript mapped types

A common use for the keyof operator is with mapped types, which transform existing types to new types by iterating through keys, often via the keyof operator.

Below is an example of how to transform the FeatureFlags type using the OptionsFlags mapped type:

type OptionsFlags<T> = {
 [Property in keyof T]: boolean;
};
// use the OptionsFlags
type FeatureFlags = { 
  darkMode: () => void;
  newUserProfile: () => void; 
};

type FeatureOptions = OptionsFlags<FeatureFlags>;
// result 
/*
type FeatureOptions = {
  darkMode: boolean; 
  newUserProfile: boolean; 
 } 
*/

In this example, OptionsFlags is defined as a generic type that takes a type parameter T. [Property in keyof T] denotes the iteration of all property names of type T, and the square bracket is the index signature syntax. Thus, the OptionsFlags type contains all properties from the type T and remaps their value to Boolean.

Using keyof with conditional mapped types

In the previous example, we mapped all the properties to a Boolean type. We can go one step further and use conditional types to perform conditional type mapping.

In the below example, we only map the non-function properties to Boolean types:

type OptionsFlags<T> = {
  [Property in keyof T]: T[Property] extends Function ? T[Property] : boolean };

type Features = {
  darkMode: () => void;
  newUserProfile: () => void;
  userManagement: string;
  resetPassword: string
 };


 type FeatureOptions = OptionsFlags<Features>;
 /**
  * type FeatureOptions = {
    darkMode: () => void;
    newUserProfile: () => void;
    userManagement: boolean;
    resetPassword: boolean;
} */

We can see how handy it is to map the Features type to a FeatureOptions type in the example. But best of all — any future changes in the source FeatureFlags type will be reflected in the FeatureOptions type automatically.

Using keyof with utility types

TypeScript provides a set of inbuilt mapped types, called utility types. The Record type is one of them. To understand how Record type works, we can look at its definition below:

// Construct a type with set of properties K of T
type Record<K extends string | number | symbol, T> = { [P in K]: T; }

As you can see, it simply returns a new type after mapping all the property keys to type T.
We can use the Record type to rewrite the previous FeatureOptions type example:

type FeatureOptions = Record<keyof FeatureFlags, boolean>; 
// result 
/* type FeatureOptions = { 
  darkMode: boolean; 
  newUserProfile: boolean; 
} 
*/

Here, we get the same FeatureOptions type using the record type to take a set of properties and transform them to Boolean type.

Another common usage of the keyof operator with utility types is with Pick type. The Pick type allows you to pick one or multiple properties from an object type, and create a new type with the chosen properties.


More great articles from LogRocket:


The keyof operator ensures that the constraint is applied so that only the valid property names can be passed into the second parameter, K:

type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
 };

The following example shows how to derive a FeatureDarkModeOption type from the FeatureOption type using Pick:

type FeatureDarkModeOption = Pick<FeatureOptions, 'darkMode'>;
 /**type FeatureDarkModeOption = {
    darkMode: boolean;
} */

Using keyof with TypeScript template string literals

Introduced in TypeScript 4.1, the template literal type allows us to concatenate strings in types. With a template literal type and keyof, we can compose a set of strings with all the possible combinations:

type HorizontalPosition = { left: number; right: number };
type VerticalPosition = { up: number; down: number };
type TransportMode = {walk: boolean, run: boolean};

type MovePosition = `${keyof TransportMode}: ${keyof VerticalPosition}-${keyof HorizontalPosition}`;
/* result
type MovePosition = "walk: up-left" | "walk: up-right" | "walk: down-left" | "walk: down-right" | "run: up-left" | "run: up-right" | "run: down-left" | "run: down-right"
*/

In this example, we create a large union type MovePosition, which is a combination of the TransportMode, HorizontalPosition, and VerticalPosition types. Creating these sorts of union types manually would make them error-prone and difficult to maintain.

Property remapping and keyof

Together with template string literal types in TypeScript 4.1, a set of utilities is provided out of the box to help with string manipulation. These utilities make it easier to construct types with remapped properties.

Here is an example:

interface Person {
  name: string;
  age: number;
  location: string;
}

type CapitalizeKeys<T> = {
  [P in keyof T as `${Capitalize<string & P>}`]: T[P];
}

type PersonWithCapitalizedKeys = CapitalizeKeys<Person>;
/* result:
type PersonWithCapitalizedKeys = {
    Name: string;
    Age: number;
    Location: string;
}
*/

In the as ${Capitalize<string & P>}, we use as to map the left side to the capitalized key, and still have access to the original key P.

You may notice that we use <string & P>, what does that mean? If we remove the string &, a compiler error will be shown as below:

Compiler Error In Typescript.

This error occurs because the Capitalize type requires the type parameter to be string | number | bigint | boolean | null | undefined. But P is a union type of string | number | symbol. The symbol type in P isn’t compatible with Capitalize.

Thus, we apply & (intersection) between our string type and P type, which returns only the string type.

Advanced property remapping use cases

We can go a step further to create more cool stuff:

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

The above example shows a new Getter type using property remapping.

In the code snippet below, we use the Getter type to create a PersonWithGetter type. The new type can help to enforce type safety for the Getter interface:

type PersonWithGetter = Getter<Person>;
/* result
type PersonWithGetters = {
    getName: () => string;
    getAge: () => number;
    getLocation: () => string;
}*/

Let’s extend the above example. Below is an AsyncGetter type. We loop through the property P from keyof, and apply a prefix of get and a suffix of Async. We also apply Promise as the return type:

type AsyncGetter<T> = {
  [P in keyof T as `get${Capitalize<string & P>}Async`]: () => Promise<T[P]>;
}

type PersonWithAsyncGetters = AsyncGetter<Person>;
/* Result:
type PersonWithAsyncGetters = {
    getNameAsync: () => Promise<string>;
    getAgeAsync: () => Promise<number>;
    getLocationAsync: () => Promise<string>;
}*/

In these examples, we derived two new types from the Person interface. We can now apply these derived types to make the code type safe and keep a consistent interface. When the Person interface changes, the change will be propagated into the derived types automatically. We will get compiler errors if the change breaks anything.

Summary

In this article, we examined the keyof operator and discussed how to use it together with generics, conditional types, and template literal types.

The keyof operator is a small but critical cog in the big TypeScript machine. When you use it in the right place with other tools in TypeScript, you can construct concise and well-constrained types to improve type safety in your code.

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

.
Yan Sun I am a full-stack developer. Love coding, learning, and writing.

6 Replies to “How to use the keyof operator in TypeScript”

  1. Looks like you forgot to add the generics parts to the feature flags examples. Should probably be:

    “`typescript
    type OptionsFlags = {
    [Property in keyof T]: boolean;
    };
    // use the OptionsFlags
    type FeatureFlags = {
    darkMode: () => void;
    newUserProfile: () => void;
    };

    type FeatureOptions = OptionsFlags;

    “`

    Or you could hard code FeatureFlags as the T in OptionsFlags like `[Property in keyof FeatureFlags]`

  2. Hey there, I wonder why my comment about the errors in some of the examples above was never posted? The mapped type examples (before the utility types) don’t work because the generic arguments and params are missing. If you don’t believe me, copy and paste the code examples into the Typescript playground: https://www.typescriptlang.org/play where you can see the errors listed.

    This actually confused me for a good bit because I consider your articles very high quality, and I’m still a little new to some of the more advanced topics in the article (which I found to be an awesome resource).

    1. Hi there, thanks for reading and for pointing out that typo. We did publish your original comment — we do moderate our comments, so there is sometimes a delay between posting and publishing. We fixed the typo in the code, so it should be all set now. If you’re still getting errors, please let us know!

      1. I waited about a week, but no problem.

        Sigh. I looks like either your comments stripped out the tags, or I somehow forgot to fix it after copying and pasting… because my code also has the same typos…

        Not sure if you trusted my code (huge mistake!), but you still need to add the “arguments” part under “Using keyof with TypeScript mapped types”, and the example that comes after (the conditional one).

        type FeatureOptions = OptionsFlags; should be

        type FeatureOptions = OptionsFlags

        Haven’t checked any of the rest, just these two examples. Hopefully this isn’t being too pedantic, just wanted to help fix a normally trustworthy source.

        1. Ok I won’t bother you guys again, but it seems your comments system isn’t very friendly to code. Aside from the fact that it doesn’t accept markup (faulty assumption on my part), it’s also pretty liberally stripping all kinds of stuff (example angle brackets and whatever is inside). I noticed this also happened to me in a separate post and comment. In both cases, the my code examples where HTML or Typescript generics were involved were stripped out. So my above correction is wrong, but the mistakes in the article are still present (you still need to add the argument to the callers).

Leave a Reply