How to iterate over types in TS ?
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 }
*/