How to Use If and Recursive Loops in TypeScript
TypeScript, a superset of JavaScript, extends the capabilities of JavaScript with its powerful type system. One of the advanced features of TypeScript is type gymnastics, a term that refers to leveraging TypeScript’s type system to perform complex type-level computations. Type gymnastics can be used to implement sophisticated logic and type transformations at compile time, such as conditional (if
) logic and recursive (for
) loops.
In this article, we will dive into how to implement conditional types (if
), and recursive types (for
) in TypeScript type gymnastics. We'll explore practical examples, and explain how to mix these concepts to create dynamic, type-safe code.
Understanding TypeScript’s Type System for Type Gymnastics
TypeScript’s type system is not just for static type-checking—it can also be used for type-level computations, which means you can write logic that affects types based on conditions, values, or other types. This opens the door to a host of possibilities, like defining types that depend on the values of other types or iterating over arrays at the type level.
In type gymnastics, we make use of the following core features:
- Conditional Types: Used to implement if-else like logic in types.
- Recursive Types: Used to simulate loops, which can be used for iterating over arrays or performing repetitive tasks at the type level.
- Tuple Operations: Utilized alongside recursion to handle arrays, map over them, or accumulate results.
Let's dive into the details.
1. Implementing Conditional (if
) Logic in TypeScript
In TypeScript, conditional types allow you to implement if
logic directly in your type definitions. Conditional types are essentially a type-based equivalent of JavaScript's ternary operator: condition ? trueValue : falseValue
. They allow us to define types that depend on a condition.
Basic Syntax for Conditional Types
The syntax for a conditional type is:
1 type ConditionalType<Condition, TrueType, FalseType> = Condition extends true ? TrueType : FalseType;
This means that if Condition
extends true
, the type will resolve to TrueType
, and if not, it will resolve to FalseType
.
Example 1: Simple Conditional Type
1 type IsTrue = ConditionalType<true, "Yes", "No">; // "Yes"2 type IsFalse = ConditionalType<false, "Yes", "No">; // "No"
Here, IsTrue
will resolve to "Yes"
, and IsFalse
will resolve to "No"
.
Example 2: Conditional Type with More Complex Logic
You can extend conditional types to perform more complex operations, such as checking whether a type extends another type. Let’s use a type to determine whether a given type is a string.
1 type IfString<T> = T extends string ? "String" : "Not String";23 type Result1 = IfString<string>; // "String"4 type Result2 = IfString<number>; // "Not String"
In this case, IfString
checks whether T
extends string
and returns "String"
if true, or "Not String"
if false.
2. Implementing Recursive (for
) Loops in TypeScript
While JavaScript has a straightforward for
loop to iterate over collections, TypeScript does not have native support for loops at the type level. However, recursive types allow us to simulate looping behavior.
Recursive types are defined by having a type refer to itself within its definition. This is essential for creating loops in TypeScript’s type system.
Basic Syntax for Recursive Types
A basic recursive type pattern is:
1 type Loop<T extends number, Result extends any[] = []> = Result["length"] extends T ? Result : Loop<T, [...Result, 1]>;
Here, Loop
recursively builds an array of length T
. The termination condition is when the array's length matches T
.
Example 1: Building an Array of Length N
1 type BuildArray<N extends number, Arr extends any[] = []> = Arr["length"] extends N ? Arr : BuildArray<N, [...Arr, 1]>;23 type ArrayOf3 = BuildArray<3>; // [1, 1, 1]4 type ArrayOf5 = BuildArray<5>; // [1, 1, 1, 1, 1]
In this example, BuildArray<3>
creates an array with three elements, and BuildArray<5>
creates an array with five elements. The recursion continues until the length of the array matches N
.
Example 2: Simulating a for
Loop to Map Over an Array
You can simulate a for
loop to map over an array and apply transformations to its elements. Here's an example that converts an array of numbers to strings:
1 type MapArray<Arr extends any[], Handler extends (item: any) => any> = Arr extends [infer First, ...infer Rest]2 ? [ReturnType<Handler>, ...MapArray<Rest, Handler>]3 : [];45 type NumberToString = (n: number) => string;6 type StringArray = MapArray<[1, 2, 3], NumberToString>; // [string, string, string]
This example demonstrates how to recursively transform each element of an array to a string. The MapArray
type takes an array (Arr
) and a handler function (Handler
) and returns a new array where each element is transformed using the handler.
3. Combining if and for in Type Gymnastics
The real power of TypeScript's type gymnastics comes when we combine conditional types (if
) and recursive types (for
). This allows us to create complex logic that is both type-safe and flexible.
Example 1: Filtering an Array Based on Type
Suppose we want to filter an array to keep only the elements of a certain type. We can combine conditional types with recursion to implement a filter
function at the type level.
1 type FilterArray<Arr extends any[], FilterType> = Arr extends [infer First, ...infer Rest]2 ? First extends FilterType3 ? [First, ...FilterArray<Rest, FilterType>] // if true, keep the element4 : FilterArray<Rest, FilterType> // if false, skip the element5 : [];67 type NumbersOnly = FilterArray<[1, "a", 2, "b", 3], number>; // [1, 2, 3]8 type StringsOnly = FilterArray<[1, "a", 2, "b", 3], string>; // ["a", "b"]
In this example, FilterArray
checks each element of the array (Arr
) to see if it extends FilterType
. If it does, the element is included in the result; otherwise, it’s skipped.
Example 2: Conditional Replacement of Array Elements
Next, let’s implement a type that conditionally replaces certain elements in an array.
1 type ReplaceIf<Arr extends any[], Condition, Replacement> = Arr extends [infer First, ...infer Rest]2 ? First extends Condition3 ? [Replacement, ...ReplaceIf<Rest, Condition, Replacement>] // if true, replace the element4 : [First, ...ReplaceIf<Rest, Condition, Replacement>] // if false, keep the element5 : [];67 type ReplacedArray = ReplaceIf<[1, "a", 2, "b", 3], number, "num">; // ["num", "a", "num", "b", "num"]
Here, ReplaceIf
checks each element in the array (Arr
). If the element matches the condition (Condition
), it is replaced with Replacement
; otherwise, it remains unchanged.
4. Advanced Type Gymnastics Patterns
Let’s explore some more advanced type gymnastics patterns by combining recursion, conditionals, and other TypeScript features.
Example 1: Counting Elements that Satisfy a Condition
You can use recursion to count how many elements in an array satisfy a given condition. Here's a CountIf
type:
1 type CountIf<Arr extends any[], Condition, Count extends any[] = []> = Arr extends [infer First, ...infer Rest]2 ? First extends Condition3 ? CountIf<Rest, Condition, [...Count, 1]> // if true, increment the counter4 : CountIf<Rest, Condition, Count> // if false, keep the counter unchanged5 : Count["length"];67 type CountNumbers = CountIf<[1, "a", 2, "b", 3], number>; // 3
The CountIf
type recursively traverses the array, incrementing the counter (Count
) when an element matches the condition (Condition
). Finally, it returns the length of the counter array, which represents the count of elements satisfying the condition.
Example 2: Grouping Elements Based on a Condition
We can also group elements in an array based on a condition using recursion and conditional types.
1 type Partition<Arr extends any[], Condition, Truthy extends any[] = [], Falsy extends any[] = []> = Arr extends [infer First, ...infer Rest]2 ? First extends Condition3 ? Partition<Rest, Condition, [...Truthy, First], Falsy> // if true, add to the Truthy group4 : Partition<Rest, Condition, Truthy, [...Falsy, First]> // if false, add to the Falsy group5 : [Truthy, Falsy];67 type Grouped = Partition<[1, "a", 2, "b", 3], number>; // [[1, 2, 3], ["a", "b"]]
The Partition
type recursively checks each element in the array and groups it into either the Truthy
or Falsy
array based on whether it satisfies the condition.
Conclusion
TypeScript’s type system provides a rich set of tools for performing type gymnastics, which allows you to implement complex logic at the type level. By using conditional types and recursive types, we can simulate if
statements and for
loops to manipulate types, perform array operations, and solve computational problems at compile time.
Through the examples provided in this article, we have demonstrated how to implement basic and advanced logic using TypeScript's type system. Combining recursion and conditionals opens up new possibilities for type-safe, flexible, and dynamic type operations. Whether you're working with arrays, performing summations, filtering elements, or manipulating types in other ways, TypeScript’s type gymnastics can help you create powerful and efficient code.