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

How to use the keyof operator in TypeScript

6 min read 1692

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 across these different sections:

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

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.

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 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 a 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 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.

The 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 conditional mapped types

In the previous example, we mapped all the properties to 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.


More great articles from LogRocket:


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.

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. If we have to create these sort of union types manually, you can imagine it will be 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]
};

This 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 the 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.

Writing a lot of TypeScript? Watch the recording of our recent TypeScript meetup to learn about writing more readable code.

TypeScript brings type safety to JavaScript. There can be a tension between type safety and readable code. Watch the recording for a deep dive on some new features of TypeScript 4.4.

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