How to iterate over types in TS ?

Thomas Rubattel
4 min readAug 2, 2023
Möbius loop. Credis: Simon Lee on Unslash

1. Array type

What we have

type CalifornianZIP = [90001, 90002, 90003, 90004, 90005];

What we want

type CalifornianZIPStr = ['90001', '90002', '90003', '90004', '90005'];

How

The idea here is to iterate over the array type by using recursion, happening until the array we build up — accumulator — , reaches the size of the original array. We can rely on conditional type for doing the recursion and on the rest Generics for building up the new Array type.

type CalifornianZIP = [90001, 90002, 90003, 90004, 90005];

type Mapping<N extends number[], Acc extends string[] = []> =
N['length'] extends Acc['length'] ?
Acc :
Mapping<N, [...Acc, `${N[Acc['length']]}`]>;

type CalifornianZIPStr = Mapping<CalifornianZIP>;

2. Union type

Use case 1

What we have

type Articles = 
| { __typename: 'Book', publisher: string }
| { __typename: 'Bike', brand: string }
| { __typename: 'Food', expirationDate: Date };

What we want

type Bike = { __typename: 'Bike', brand: string };

How

The idea here is to iterate over the union type by leveraging the distributivity of conditional type.

type Articles = 
| { __typename: 'Book', publisher: string }
| { __typename: 'Bike', brand: string }
| { __typename: 'Food', expirationDate: Date };

type FilterBike<A> = A extends { __typename: 'Bike' } ? A : never;

type Bike = FilterBike<Articles>;

// 💡 the utility type `Extract` does the same under the hood
// type Bike = Extract<Articles, { __typename: 'Bike' }>;

Use case 2

What we have

type Events = 'change' | 'blur' | 'click' | 'scroll'; 

What we want

type EventHandlers = 'onChange' | 'onBlur' | 'onClick' | 'onScroll';

How

The idea here is to iterate over the union type by using the feature called template literal type.

type Events = 'change' | 'blur' | 'click' | 'scroll';

type EventHandlers = `on${Capitalize<Events>}`;

3. Object type

What we have

type Duration = {
year: number,
month: number,
week: number,
day: number,
minute: number,
second: number,
millisecond: number,
};

What we want

type DurationNewProps = {
Years: number,
Months: number,
Weeks: number,
Days: number,
Minutes: number,
Seconds: number,
Milliseconds: number,
};

How

The idea here is to iterate over the object type properties by using the feature called Mapped Type. The renaming of properties is empowered by the feature called key remapping.

type Duration = {
year: number,
month: number,
week: number,
day: number,
minute: number,
second: number,
millisecond: number,
};


type DurationNewProps = {
[K in keyof Duration as `${Capitalize<K>}s`]: Duration[K];
};

Takeaway

In this short article, we have discussed on how to loop through the most common compound type structures, namely array type, union type and object type.

The manipulation of types in a programmatic way is called type-level programming. Over the time, TS introduced many features, as we saw, — generics (TS 0.9), conditional type (TS 2.8), template literal type (TS 4.1), mapped type (TS 2.1), key remapping (TS 4.1), rest Generics (TS 4.0), never type (TS 2.0), tail-recursion (TS 4.5) — for making type-level programming possible.

One feature that is still in demand towards type-level programming is the possibility to pass a Generic type as parameter of a Generic type, the same way one would pass a callback as parameter to a function. This feature, known as higher order type, is supported by some libraries, such as free-types. With respect to the first example, it would have been nice to decouple the iteration and the transformation by passing a Generic type for that transformation — stringification— to the utility type Mapping.

I wrote a more comprehensive article on TypeScript in general, you may have a look at it for a larger overview on TypeScript’s capabilities.

Bonus (pro memoria)

Below you get how the resolution of conditional type with union type works under the hood, step-by-step.

type Articles = 
| { __typename: 'Book', publisher: string }
| { __typename: 'Bike', brand: string }
| { __typename: 'Food', expirationDate: Date };

type FilterBike<A> = A extends { __typename: 'Bike' } ? A : never;

type Bike = FilterBike<Articles>; // { __typename: 'Bike', brand: string }

/*
type Bike = FilterBike<Articles>

<=>

type Bike = FilterBike<
| { __typename: 'Book', publisher: string }
| { __typename: 'Bike', brand: string }
| { __typename: 'Food', expirationDate: Date }
>

<=>

FilterBike<{ __typename: 'Book', publisher: string }> |
FilterBike<{ __typename: 'Bike', brand: string }> |
FilterBike<{ __typename: 'Food', expirationDate: Date }>

<=>

never | { __typename: 'Bike', brand: string } | never

<=>

{ __typename: 'Bike', brand: string }
*/

--

--