Challenge

Implement a type IsUnion, which takes an input type T and returns whether T resolves to a union type. For example:

type case1 = IsUnion<string>; // false
type case2 = IsUnion<string | number>; // true
type case3 = IsUnion<[string | number]>; // false

Solution

When I’m seeing challenges like this one, I always get frustrated. Because there is no general solution we could use for implementing such a type. There are no built-in types or intrinsic that we could use.

So that we must be creative and use what we can. Let us start by thinking about unions and what they represent.

When you specify a plain type, e.g. string, it never will be anything else but string. Although, when you specify a union, e.g. string | number, you get a set of potential values from the string and the number.

Plain types do not represent a set of values collectively, while unions do. There is no sense in distributive iteration on plain types, but it is for unions.

And that is the key difference, how can we detect if the type is union. When iterating distributively over the type T, which is not a union, it changes nothing. But it changes a lot if it is a union.

TypeScript has a wonderful type feature - distributive conditional types. When you write the construct T extends string ? true : false, where T is a union, it will apply the condition distributively. Roughly, it looks like different conditional types for each element from the union.

type IsString<T> = T extends string ? true : false;

// For example, we provide type T = string | number
// It is the same as this
type IsStringDistributive = string extends string
  ? true
  : false | number extends string
  ? true
  : false;

You see where I’m heading with this? If the type T is a union, by using distributive conditional types we can split the union and compare it against the input type T. In case, they are the same - it is not a union. But, when it is a union, they will not be the same, because string does not extend from string | number and number does not extend from string | number.

Let us start with implementation already! At first, we will make a copy of input type T, so we can preserve the input type T with no further modifications. We will compare them to each other later.

type IsUnion<T, C = T> = never;

By applying the conditional type, we get the distributive semantics. Inside the “true” branch of the conditional type, we will get each item from the union.

type IsUnion<T, C = T> = T extends C ? never : never;

Now, the most important part - compare the item with the original input type T. In case, these types are the same, it means no distributive iteration was applied - not a union, hence false. Otherwise, distributive iteration was applied and we compare the single item from the union with the union itself, meaning it is a union, hence true.

type IsUnion<T, C = T> = T extends C ? ([C] extends [T] ? false : true) : never;

Done! To clarify things, let me show you what [C] and [T] hold in “true” branch of the distributive conditional type.

When we pass not a union, e.g. string, they hold the same types. Meaning, it is not union; we return false.

[T] = [string][C] = [string];

But, if we pass a union, e.g. string | number, they hold different types. While our copy C holds a tuple with a union inside, our T holds a union of tuples, thanks to distributive conditional types, hence it is a union.

[T] = [string] | [number]
[C] = [string | number]

References