5 JavaScript Utility Types — And How To Create Your Own | by Vladimir Topolev | May, 2022

Advanced TypeScript: from zero to hero

Photo by Lautaro Andreani on Unsplash

TypeScript has a bunch of useful utility types. But do you know how to create them on your own? I believe if you recreate each of them, you will know advanced TypeScript features pretty well. If it’s still an issue, you have space to improve your skill.

I encourage you to go along with the built-in utility types list and try to recreate them on your own. It helps you reveal some gaps in your TypeScript knowledge, and it covers all advanced TypeScript features.

Let’s discuss how I would recommend reading this article to get more benefits. First, I will provide some basic knowledge about specific TypeScript features, and after that, some relevant examples from utility types we need to implement on our own. Take some time to implement it before you have a look at the answer.

The main aim here is to provide all the necessary information needed to allow us to reimplement each utility type from scratch.

Before we go too far, this article assumes you have knowledge of Generics. If you are uncomfortable with it, don’t worry, you may have a look at this article.

Mapped types build on the syntax for index signatures which are used to declare the types of properties. Let’s assume we need to create a type for an object that contains only boolean properties, which you can see in the example below:

type OnlyBooleanProperties = {
[Key: string]: boolean
}

Index signature types are frequently used together with keyof and in keywords. Let’s assume we have an object and we would like to create a new one with the same properties, but each property contains values ​​with boolean type, as shown below:

type OptionsFlags<Type> = {
[Key in keyof Type]: boolean
}

In this example, OptionsFlags will take all the properties from the type Type and change their values ​​to be a boolean.

Example 1

Let’s have a look at the utility type Record<Keys, Type> (link here). It builds an object type whose property keys are Keys and whose property values ​​are Type. If there were no such utility, how would we create it on our own? You’ve already gotten the necessary knowledge on how to complete it, therefore, take some time to do it on your own. After that, check the answer below:

type Record<Keys extends keyof any, Type> = {
[Key in Keys]: Type
}

Example 2

Let’s have a look at the utility type Pick<Type, Keys> (link here). It builds a type by picking the set of properties Keys from Type. Here’s the code:

type Pick<Type, Keys extends keyof Type> = {
[Key in Keys]: Type[Key]
}

Two additional modifiers can be applied during mapping: readonly and ?which affect mutability and optionality, respectively.

To see this in action, let’s have a look at Partial<Type> (link here). It builds a type with all properties of Type set to optional, which you can see below:

type Partial<Type> = {
[Key in keyof Type]?: Type[Key];
};

You can remove modifiers readonly and ? by prefixing with - .

Here’s another example; it uses Required<Type> (link here) and builds a type consisting of all properties of Type set to required. Here’s the code:

type Required<Type> = {
[Key in keyof Type]-?:Type[Key]
}

Example 3

In the previous examples, we covered all utility types with a ? modifier. To get more practice, you need to implement Readonly<Type> to use areadonly modifier (link here). It constructs a type with all properties of Type set to readonly.

type Readonly<Type> = {
readonly [K in keyof Type]: Type[K]
}

We all know ternary conditions in JS code, and it turns out that we can use the same syntax to define types. The main difference here is that we will use the expression with the extends keyword for TS ternary for a conditional part. This condition looks like T extend K. Here’s the code:

SomeType extends OtherType ? TrueType : FalseType;

When the type on the left of the extends is assignable to the one on the right, then you’ll get the type in the first branch (the “true” branch); otherwise, you’ll get the type in the latter branch (the “false” branch). For example,

10 extends number ? 'YES' : 'NO'
=> 'YES' // since 10 extends number === true
10 extends string ? 'YES' : 'NO'
=> 'NO' // since 10 extends string === false

Let’s have a look at another example:

type StringFromType<T> = T extends string ? 'string' : never;type Type1 = StringFromType<"text">;  // string
type Type2 = StringFromType<10>; // never

Here we need to give some words about never type. It literally means “no value,” and it is treated as an empty set . usually, never uses in situations and control flows that should not logically happen. You will often see it being used as a dead-end type, like in the example above.

Also, I would like to highlight how the never behaves in a union type like this one string | never since the never means an empty set (). Therefore the union of any set with the empty set is the set we started with — X U ∅ = X. It means the following:

string | never     => string;
number | never => number;
<AnyType> | never => AnyType;

We can also chain more conditions exactly like nesting ternary operators in JavaScript, which you see below:

type StringFromType<T> = 
T extends string
? 'string'
: T extends number
? 'number'
: never;
type Type1 = StringFromType<"text">; // string
type Type2 = StringFromType<10>; // number
type Type3 = StringFromType<{}>; // never

In most cases, you wouldn’t like to reach a dead-end type never. Actually, it’s better to put a constrain on the generic T using the extend keyword and list only allowed types like this:

type StringFromType<T extends string | number> = 
T extends string
? 'string'
: T extends number
? 'number'
: never;

In this case, TypeScript doesn’t even allow you to use this type with types other then number or string and therefore, there’s no way to reach never . It looks like the following:

In the case of extending a union as a constraint, TypeScript will loop over each member of the union and return a union of its own. What does it mean? Let’s assume that we have the following condition type:

type NonNullable<Type> 
= Type extends null | undefined ? never : Type;

How does TypeScript calculate it if we pass a Generic type instead of a union type with the following shape string | null | undefined . It loops over each member as explained above, and it may be rewritten in this way:

type ReturnedType = NonNullable<string | null | undefined>
= (string extends null | undefined ? never : string)
| (null extends null | undefined ? never : null)
| (undefined extends null | undefined ? never : undefine)
= string | never | never
= string

By the way, we’ve already implemented another build-in NonNullable<Type> (link here) and we’ve already known how it works. Congrats.

Example 4

Let’s have a look at the utility type Exclude<UnionType, ExcludedMembers> (link here). It creates a new type by excluding UnionType from all union members assignable to ExcludedMembers.

type Exclude<UnionType, ExcludedMembers> 
= UnionType extends ExcludedMembers ? never : UnionType

To be honest, for me, it was not so easy to wrap my head around. Therefore, let’s loop over each union type member like a TypeScript transpiler did and calculate a condition for each of them.

type NewType = Exclude<'a' | 'b' | 'c', 'a'>
= ('a' extends 'a' ? never : 'a')
| ('b' extends 'a' ? never : 'b')
| ('c' extends 'a' ? never : 'c')
= never | 'b' | 'c'
= 'b' | 'c'

Example 5

Wow, if Example 4 is clear for you, I believe you will sort out the next utility type: Extract<UnionType, ExtractedMembers> (link here). It created a type by extracting from UnionType all union members that are assignable to ExtractedMembers.

type Extract<UnionType, ExtractedMembers>
= UnionType extends ExcludedMembers ? UnionType: never

We just swapped the right and left operands in the ternary operation. Let’s think about how a TypeScript transpiler works under the hook against this example:

type NewType = Extract<'a' | 'b' | 'c', 'a' | 'c'>
= ('a' extends 'a' | 'c' ? 'a' : never)
| ('b' extends 'a' | 'c' ? 'b' : never)
| ('c' extends 'a' | 'c' ? 'c' : never)
= 'a' | never | 'c'
= 'a' | 'c'

Example 6

This example is not relevant to the distributive conditional type topic, but it builds on top of the Exclude<UnionType, ExcludedMember> type we’ve implemented above (example 4). And it’s an amazing chance to recall it. Well, it is Omit<Type, OmittedKeys> (link here), and it creates a type by picking all properties from Type and then removing OmittedKeys.

type Omit<Type, OmmitedKeys extends keyof Type> = {
[Key in Exclude<keyof Type, OmmitedKeys>]: Type[Key]
}

I hope it’s done well, and we haven’t forgotten the Mapped Type topic we discussed in the first section.

Probably, the most interesting part of conditional types is that we can infer the new type from a part of a conditional statement that may be used in a conditional type afterward in “true” or “false” branches. Don’t be scared; it sounds intimidating until we don’t see a particular example. So let’s take a look at one:

type IdType<T> = T extends { id: infer ID} ? ID: never;

In this example, we infer the type ID from the id field of type T. If T has the id property, TypeScript infers the type of that property as ID and we can use it in “true” or “false” branches.

Example 7

Let’s have a look at ReturnType<Type> (link here) that constructs a type consisting of the return type of function.

type ReturnType<T extends (...args: any) => any> 
= T extends (...args: any) => infer R ? R : any;

Example 8

It’s not included in the TypeScript utility type, but sometimes it may be pretty handy, especially when you’re trying to extend any React component from the 3d party library that doesn’t extend property type.

In most cases, we adhere to a functional approach and a React Component is presented as a function where the first argument is a component props. We need to infer a type of Component props.

const Component = (props: { prop1: string }) => null;// should extract type of props: {prop1: string}
type T0 = ComponentProps<typeof Component>

Take some time to implement it on your own, but the approach is similar to the approach in Example 7.

type ComponentProps<T extends (arg: any) => any> 
= T extends (arg: infer Props) => any ? Props : never;

The difference with Example 7 is a place from which we infer type in a function declaration. By the way, this type is already implemented in react with the same name, but their type takes into account Functional and Class Components. Here’s the code:

import { ComponentType } from 'react'

Thanks for reading!

I hope you enjoyed reading this article.

Leave a Comment