This is a premium alert message you can set from Layout! Get Now!

How to use the keyof operator in TypeScript

0

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:

  • With TypeScript generics
  • With TypeScript mapped types
  • With TypeScript template string literal types

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

type FeatureOptions = OptionsFlags;
// 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 = {
  [Property in keyof T]: T[Property] extends Function ? T[Property] : boolean };

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


 type FeatureOptions = OptionsFlags;
 /**
  * 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.

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 = {
  [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.

The post How to use the <code>keyof</code> operator in TypeScript appeared first on LogRocket Blog.



from LogRocket Blog https://ift.tt/W7wMn04
via Read more

Post a Comment

0 Comments
* Please Don't Spam Here. All the Comments are Reviewed by Admin.
Post a Comment

Search This Blog

To Top