JavaScript Development Space

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:

  1. Conditional Types: Used to implement if-else like logic in types.
  2. Recursive Types: Used to simulate loops, which can be used for iterating over arrays or performing repetitive tasks at the type level.
  3. 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:

ts
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

ts
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.

ts
1 type IfString<T> = T extends string ? "String" : "Not String";
2
3 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:

ts
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

ts
1 type BuildArray<N extends number, Arr extends any[] = []> = Arr["length"] extends N ? Arr : BuildArray<N, [...Arr, 1]>;
2
3 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:

ts
1 type MapArray<Arr extends any[], Handler extends (item: any) => any> = Arr extends [infer First, ...infer Rest]
2 ? [ReturnType<Handler>, ...MapArray<Rest, Handler>]
3 : [];
4
5 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.

ts
1 type FilterArray<Arr extends any[], FilterType> = Arr extends [infer First, ...infer Rest]
2 ? First extends FilterType
3 ? [First, ...FilterArray<Rest, FilterType>] // if true, keep the element
4 : FilterArray<Rest, FilterType> // if false, skip the element
5 : [];
6
7 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.

ts
1 type ReplaceIf<Arr extends any[], Condition, Replacement> = Arr extends [infer First, ...infer Rest]
2 ? First extends Condition
3 ? [Replacement, ...ReplaceIf<Rest, Condition, Replacement>] // if true, replace the element
4 : [First, ...ReplaceIf<Rest, Condition, Replacement>] // if false, keep the element
5 : [];
6
7 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:

ts
1 type CountIf<Arr extends any[], Condition, Count extends any[] = []> = Arr extends [infer First, ...infer Rest]
2 ? First extends Condition
3 ? CountIf<Rest, Condition, [...Count, 1]> // if true, increment the counter
4 : CountIf<Rest, Condition, Count> // if false, keep the counter unchanged
5 : Count["length"];
6
7 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.

ts
1 type Partition<Arr extends any[], Condition, Truthy extends any[] = [], Falsy extends any[] = []> = Arr extends [infer First, ...infer Rest]
2 ? First extends Condition
3 ? Partition<Rest, Condition, [...Truthy, First], Falsy> // if true, add to the Truthy group
4 : Partition<Rest, Condition, Truthy, [...Falsy, First]> // if false, add to the Falsy group
5 : [Truthy, Falsy];
6
7 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.

JavaScript Development Space

© 2025 JavaScript Development Space - Master JS and NodeJS. All rights reserved.