Skip to content

Abusing TypeScript's Type System

Posted on:February 1, 2024 at 07:16 PM

So it all started with this tweet by my good friend Jamon who had an interesting question:

Click Here for Original


Utility Types: Exclude

In TypeScript, Exclude is a built-in utility type. It is not a keyword but a predefined type in the TypeScript standard library. The Exclude utility type is used to create a new type by excluding one set of types from another.

Here’s a simplified explanation:

type Exclude<T, U> = T extends U ? never : T;

Exclude<T, U> produces a type that includes all the types from T that are not assignable to U. It utilizes conditional types to filter out types.

In the context of the original question and the code samples, the usage of Exclude<Low, High> is part of a type-level computation where it is employed to increment Low by 1 in the process of creating a range of numbers.

To clarify, Exclude is not being used in the standard way here; it’s being leveraged creatively in the context of defining a range of numbers within TypeScript’s type system. This usage is more of a convention or a specific implementation detail rather than a standard or documented behavior of the Exclude utility type.


Understanding Recursive Types: Range<Low, High>

So the answer given was actually:

type Range<Low, High> = Low extends High ? never : Low | Range<Exclude<Low, High>, High>;

The Range<Low, High> type is a recursive type that generates a union of numbers from Low to High. When Low equals High, the recursion gracefully ends with a return type of never. Otherwise, it forms a union of Low and the result of calling Range with Low incremented by 1 and High unchanged.

Decoding the Magic of Exclude<Low, High>

type Exclude<Low, High> = Low extends High ? never : Low + 1;

Now, let’s unravel the mysteries of Exclude<Low, High>. This utility type is pivotal in incrementing Low by 1. It becomes instrumental in crafting types like our beloved ZeroToHundred, a union encompassing all numbers from 0 to 100.

type ZeroToHundred = Range<0, 100>;

But why the incrementation? The answer lies in TypeScript’s remarkable type system and its prowess in generating unions through recursive types. When Exclude<Low, High> is employed, it empowers TypeScript to construct a new union spanning all possible numbers between Low and High, ensuring that Low gracefully steps up by 1.

This design choice leads to cleaner, more concise type definitions in our code. With the assistance of the Exclude<Low, High> utility type, we can effortlessly generate a comprehensive range of numbers without the need to explicitly list each individual one.


Empowering Efficient Type Definitions

In summary, Exclude<Low, High> is the unsung hero that facilitates the incremental dance of Low in TypeScript’s Range<Low, High> and other recursive types.

// Example usage:
const numberInRange: ZeroToHundred = 42; // Valid, as 42 is in the range 0 to 100
const outsideRange: ZeroToHundred = 150; // Error, as 150 is outside the range 0 to 100

This approach not only enhances efficiency but also provides manageability, especially when dealing with expansive ranges of numbers.

So, the next time you encounter a recursive type in your TypeScript journey, embrace the enchantment of Exclude<Low, High>. Let it be your guide to crafting elegant and powerful type definitions. Happy coding, fellow TypeScript enthusiasts! 🤖