keyof
operator in TypeScriptEditor’s note: This article was last updated on 27 November 2023 to discuss the keyof typeof
pattern, and using keyof
to create new types based on Object.keys
.
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 will examine the keyof
operator and how it is commonly used 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.
keyof
operatorThe 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 shown in the above examples, it’s less useful when applied to primitive types.
Object.keys
vs. keyof
operatorIn JavaScript, Object.keys
are used to return an array of object keys. In the code below, 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, we 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 }; console.log(Object.keys(user)); // output: ["name", "age"]
But, when we iterate the keys and access the object property by the key, TypeScript throws an error when the TypeScript strict mode is turned on:
Object.keys(user).forEach(key => { console.log(user[key]) // error is shown })
The error is because we tried to use the string
type key to access the object with union type “name
” | “age
“.
You might wonder why TypeScript doesn’t return typed keys as “name
” | “age
“. This is intentional. Anders Hejlsberg explains the reason in this GitHub comment.
In a nutshell, the strongly typed Object.keys
is 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 work around 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 merging:
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 Hejlsberg’s comment. So use these workarounds with caution. We only want to use them when we are sure that additional properties won’t be added to the object at runtime; otherwise, it may cause an unexpected crash.
keyof typeof
patternWe often combine keyof
and typeof
together, to create a type that represents the keys of a specific object. This is particularly useful when you want to define a type based on the structure of an existing object.
Let’s say we have a userProfile
object as shown here:
const userProfile = { username: 'john_doe', email: '[email protected]', age: 30, isAdmin: false, };
We can use the keyof typeof
pattern to create a type representing the keys of this user profile object:
type UserProfileKeys = keyof typeof userProfile; // type UserProfileKeys = "username" | "email" | "age" | "isAdmin"
The UserProfileKeys
type is a union of literal types containing the keys username
, email
, age
, and isAdmin
. This type can be useful for creating functions or components that need to work with various user profile properties in a type-safe manner:
function getUserInfo(key: UserProfileKeys): any { return userProfile[key]; } const usernameValue = getUserInfo('username'); // Type-safe access
In the above example, the getUserInfo
function takes a key
parameter constrained to the UserProfileKeys
type, ensuring that only valid keys of userProfile
can be passed. This helps prevent runtime errors and enhances the overall type safety of our code.
keyof
to create new types based on Object.keys
We can use keyof
operator to derive new types based on the object keys. For example, we have an object that represents medicine below:
const medicineObject = { name: 'Aspirin', dosage: 500, manufacturer: 'ExamplePharma' };
We want to create a new type ExtendedMedicineType
that includes an additional description
property for each key in medicineObject
:
type MedicineKeys = keyof typeof medicineObject; type ExtendedMedicineType = { [K in MedicineKeys]: { value: typeof medicineObject[K]; description: string; }; };
With the help of keyof typeof
, we create a new type: MedicineKeys
. Then, we derive an ExtendedMedicineType
based on the MedicineKeys
, which ensures that each property has a specific structure (value
and description
). The newly added description
property serves as documentation for each property.
We can use the new ExtendedMedicineType
as shown below:
const myMedicine: ExtendedMedicineType = { name: { value: 'Aspirin', description: 'Name of the medicine' }, dosage: { value: 500, description: 'Dosage of the medicine in milligrams' }, manufacturer: { value: 'ExamplePharma', description: 'Manufacturer of the medicine' } };
keyof
with TypeScript genericsThe 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 we are new to TypeScript, the above function may look 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 onlyextends
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 typesobj[key]
returns the same type that the property hasWe 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.
keyof
with TypeScript mapped typesA 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.
keyof
with conditional mapped typesIn 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 example below, 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.
keyof
with utility typesTypeScript 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 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 the Pick
type. The Pick
type allows us 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; } */
keyof
with TypeScript template string literalsIntroduced 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
with the help of the keyof
operator, 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.
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 line eight, 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:
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.
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 apply these derived types to make the code type-safe and keep a consistent interface. When the Person
interface changes, the change will propagate into the derived types automatically. We will get compiler errors if the change breaks anything.
In this article, we examined the keyof
operator and discussed using it with generics, conditional types, and template literal types.
The keyof
operator is a small but critical cog in the giant TypeScript machine. When we use it in the right place with other tools in TypeScript, we can construct concise and well-constrained types to improve type safety in our code.
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.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]
6 Replies to "How to use the <code>keyof</code> operator in TypeScript"
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]`
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).
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!
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.
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).
Thanks again for your feedback. The error in question has been fixed.