10

More advanced types with TypeScript generics

 4 years ago
source link: https://wanago.io/2020/02/24/more-advanced-types-with-typescript-generics/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Previously, we’ve discussed the basics of TypeScript Generics . This time, we take them to a higher level. In this article, we learn about index types . To do so, we also explore  union types , the  keyof  keyword, and  string literal types . Today we also learn  mapped types and use them with  conditional types.

Index types

Generics are very useful in many situations. By using index types, we can improve our types even further. To get to know them, let’s first learn what a union type and the keyof keyword are.

A union type represents one of several types. To separate them, we use a vertical bar.

const value: string | number;

String literal typesare often used with unions. A string literal can only be assigned a particular string value. It can be considered a subtype of a string type.

type UserRole = 'admin' | 'moderator' | 'author';

Now, let’s consider this simple interface:

interface User {
  id: number;
  name: string;
  email: string;
  role: UserRole;
}

By using the keyof keyword, we can achieve a union of string literal types.

type UserKeysType = keyof User; // 'id' | 'name' | 'email' | 'role' ;

All of the above knowledge gives us quite a bit of flexibility.

Introducing index types

TypeScript 2.1 introduced index types . They look the same as accessing a property of an object but refer to types.

type IdType = User['id']; // number

We sometimes refer to index types as lookup types .

We can use the above in a dynamic manner. Let’s inspect this popular example:

const user: User = {
  id: 15,
  name: 'John',
  email: '[email protected]',
  role: 'admin'
};
function getProperty<ObjectType, KeyType extends keyof ObjectType>(
  object: ObjectType,
  property: KeyType
): ObjectType[KeyType] {
  return object[property];
}

When we use our getProperty , the compiler checks if a string that we pass into it is an actual property of an object.

getProperty(user, 'property');
Argument of type ‘”property”‘ is not assignable to parameter of type ‘”id” | “name” | “email” | “role”‘.

When we return object [ property ] , the TypeScript compiler performs a type lookup. Thanks to that, the return type of the  getProperty varies based on the passed string.

getProperty(user, 'id').toLowerCase();
Property ‘toLowerCase’ does not exist on type ‘number’.

Creating a Map from an object

A real-life example of the above might be converting an object to a Map. TypeScript aside, the most straightforward way to do this is to use Object.entries .

const settings = {
  isModalOpened: true,
  canDelete: false,
  role: 'Admin'
}
 
const settingsMap = new Map(
  Object.entries(settings)
);

The above code, even though valid, does not produce the most detailed types.

settingsMap.get('role').toLowerCase()
Property ‘toLowerCase’ does not exist on type ‘string | boolean’.

The error above indicates that the return type of the settingsMap . get function is a union type  'string | boolean' . We know that the type of  role is a string. Let’s fix that!

We can create our own interface that extends Map and provides more detailed typings.

interface MapFromObject<ObjectType, KeyType extends keyof ObjectType> extends Map<KeyType, ObjectType[KeyType]> {
  get: <PropertyType extends keyof ObjectType>(key: PropertyType) => ObjectType[PropertyType];
}
const settingsMap = new Map(
  Object.entries(settings)
) as MapFromObject<Settings, keyof Settings>;

Now, every time we use the get method, we get an exact type of property.

settingsMap.get('role').toLowerCase(); // 'admin'
If you have some other solution to the above issue, feel free to share it

If you need, you can also provide types for the set function in a similar manner.

Mapped types

The mapped types allow us to create new types from existing ones. A common use case is to make all of the properties of an object read-only .

type Readonly<ObjectType> = {
  readonly [KeyType in keyof ObjectType]: ObjectType[KeyType];
}

The above is such a common use-case that we now have a Readonly type built-in and ready to use.

An example of its usage is the Object.freeze function. Let’s look into how TypeScript handles it:

interface ObjectConstructor {
  /**
   * Prevents the modification of existing property attributes and values,
   * and prevents the addition of new properties.
   * @param o Object on which to lock the attributes.
   */
  freeze<T>(o: T): Readonly<T>;
}

As we can see, the Object.freeze function returns the object that is mapped using the Readonly modifier.

TypeScript developers identified more useful modifiers that might come in handy, such as Pick .

/**
 * From T, pick a set of properties whose keys are in the union K
 */
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

Even though we have a set of modifiers for different occasions, we might need to write some custom ones. When doing so, conditional types might be of some use.

Conditional type selects one of two types based on a condition
type WithNumbersInsteadOfStrings<ObjectType> = {
  [PropertyType in keyof ObjectType]: ObjectType[PropertyType] extends number ? string : ObjectType[PropertyType];
};
const user: WithNumbersInsteadOfStrings<User> = {
  id: '15',
  name: 'John',
  email: '[email protected]',
  role: 'admin'
};

Summary

In this article, we’ve expanded more on the subject of generics in TypeScript. We’ve investigated the indexed types and mapped types. When doing so, we’ve also learned the keyof  keyword, the union types . We’ve also stumbled upon  string literal types and  conditional types . Learning all of the above will definitely expand our TypeScript knowledge!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK