Avoiding distribution
Before we wrap up, I want to teach you one more trick: preventing distribution from happening. This can be useful if, for example, you want to check if a union is assignable to another one:
type Extends<A, B> = A extends B ? true : false;
// 👆
// A is distributed here.
type T = Extends<"a" | "b" | "c", "a" | "b">; // => `boolean`
// `Extends` returns boolean because "a" and "b" are
// assignable to "a" | "b", but "c" isn't.
It would make more sense for Extends
to return false
instead of boolean
in this case, but how can we fix it?
As we’ve seen in the previous section, we have several ways to prevent distribution from happening. The simplest is to wrap your union in a data structure before using extends
:
type Extends<A, B> = [A] extends [B] ? true : false;
// 👆
// Wrapping `A` in a tuple prevents
// the union from distributing.
type T = Extends<"a" | "b" | "c", "a" | "b">;
// => false 🎉
In this example, [A]
evaluates to ["a" | "b" | "c"]
. The union that used to be at the top level is now nested. Since TypeScript only distributes expressions over top-level union type variables, wrapping a union in a tuple prevents this behavior.
This means we can access our undistributed union in one of our branches if we want to:
type CreateEdge<Id, Condition> =
[Id] extends [Condition]
? { source: Id; target: Id }
/* 👆 👆
Here, `Id` is the
undistributed union type.
*/
: never;
type Edge1 = CreateEdge<"1" | "2" | "3", `${number}`>;
// => { source: "1" | "2" | "3"; target: "1" | "2" | "3" }
type Edge2 = CreateEdge<"a" | "b" | "c", `${number}`>;
// => never
const edge: Edge1 = { source: "1", target: "2" };
// ^ ✅ type-checks!
If we had written Id extends Condition
instead, The last line wouldn’t have type-checked because the conditional type would have been distributed over Id
and only edges where source
and target
are the same would have been permitted. Try it yourself!