A quick look at how TypeScript conditional types behave with union types.

I am digging deeper into TypeScript which reminds me of my Haskell days. It has been a while and I don't remember much Haskell now, but the good feeling stays and so do my static type skills which I promptly decided to exercise now with TypeScript.

Unions

We can combine any two types into a union type.

type StringOrNumber = string | number;

Or with TypeScript's literal types:

type UserRole = "employee" | "admin" | "superadmin";

Generics

Assume we have entities like User, Company, Course, etc., we can create an ApiResponse:

type ApiResponse = {
    data: User | Company | Course;
    status: number;
}

But then every time we add a new entity in our system, we also need to update the ApiResponse. Instead we can use generics:

type ApiResponse<T> = {
    data: T;
    status: number;
}

And then use it like ApiResponse<User>, ApiResponse<Company>, ApiResponse<Course>.

Conditional Types

For example, if we have types:

type Employee = {...};
type Admin = Employee & {...};
type SuperAdmin = Admin & {...};

We can create separate permissions for all admins (which includes Admin and SuperAdmin) using conditional types.

type Permissions<T> = T extends Admin ? AdminPermissions : EmployeePermissions;

Distributive Conditional Types

Type parameters can be instantiated with any type, including unions:

type Users = Array<User | Admin | SuperAdmin>;

Now Users is of type (User | Admin | SuperAdmin)[]. Which means it is an array whose elements could be a mix of any of those three types of users. But what if for some reason we want the type to represent one homogeneous array shape instead? That is, User[] | Admin[] | SuperAdmin[].

Essentially we want to "loop" over the union (User | Admin | SuperAdmin) and make each type an array. Conditional types have this "looping" behavior, formally called distributivity. We can use this to our benefit here:

type StrictArray<T> = T extends unknown ? T[] : never;

type Users = StrictArray<User | Admin | SuperAdmin>;

That T extends unknown seems redundant, but it is a dummy conditional that activates distributivity. Because T appears directly on the left side of extends, TypeScript applies the conditional to each member of the union, giving us our desired type User[] | Admin[] | SuperAdmin[].