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:
- What is TypeScript
keyof
? - TypeScript
keyof
: Object Type Keys vs Object Keys - TypeScript
keyof
: Advanced Use Cases
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 string
s 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 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 key
s 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 string
s 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<>
.