Abusing Ts Type System
So it all started with this tweet by my good friend Jamon who had an interesting question:
This should be performant for a range as low as 1-100 pic.twitter.com/s9UrfrJ6zV
— Johny Hoffman (@eclecticjohny) January 28, 2024
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! 🤖