Skip to main content

TS Ep 43: Preventing Distribution (Tuples)

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
typescript - This article is part of a series.
Part 43: This Article
Distribution is usually exactly what you want when mapping over unions. But when it isn’t, you need to know how to disable it.

1. The Problem: Unwanted Distribution
#

Let’s revisit the ToArray example from the previous episode.

type ToArray<T> = T extends any ? T[] : never;

// Distributes into: string[] | number[]
type Distributed = ToArray<string | number>; 

What if we don’t want string[] | number[]? What if we are trying to construct a single array that can hold a mix of strings and numbers: (string | number)[]?

Because T is a “naked” generic parameter in the extends clause, TypeScript forcefully distributes it.


2. The Solution: Tuple Wrapping [T]
#

To disable distribution, you must ensure the generic type parameter is no longer “naked”. The standard, idiomatic way to achieve this in TypeScript is to wrap both sides of the extends clause in a tuple (brackets []).

// 🟢 Non-Distributive Conditional Type
type NonDistributiveToArray<T> = [T] extends [any] ? T[] : never;

// Evaluates the union as a single block!
type UnifiedArray = NonDistributiveToArray<string | number>;

/* Inferred Type: 
  (string | number)[]
*/

By wrapping T in brackets, we are telling the compiler: “Do not evaluate the individual elements of this union. Evaluate the union as a single structural tuple [string | number].”

Because [string | number] extends [any], the condition resolves to true, and it returns T[] (where T is still intact as the full union).


3. Real-World Mastery: Building IsUnion<T>
#

A classic advanced TypeScript interview question is:

“Write a generic type that returns true if T is a union, and false otherwise.”

You can solve this elegantly by intentionally pitting the distributive behavior of T against the non-distributive behavior of T!

// 1. T extends any (Distributive step: Splits the union into individual types)
// 2. [U] extends [T] (Non-distributive step: Compares the original full union against the distributed piece)
type IsUnion<T, U = T> = T extends any 
  ? ([U] extends [T] ? false : true) 
  : never;

type Test1 = IsUnion<string>;           // false
type Test2 = IsUnion<string | number>;  // true

How the Magic Works:
#

Let’s trace IsUnion<string | number>:

  1. U defaults to the full union: "string | number".
  2. T extends any triggers distribution! It loops twice: once for string, once for number.
  3. Loop 1 (T is string):
    • [string | number] extends [string] ➡️ False!
    • Returns true.
  4. Loop 2 (T is number):
    • [string | number] extends [number] ➡️ False!
    • Returns true.
  5. The result recombines: true | true ➡️ true.

If we pass a single type like string:

  1. U is "string".
  2. T doesn’t have multiple members, so it just evaluates once for string.
  3. [string] extends [string] ➡️ True!
  4. Returns false.

Summary & Next Steps
#

In this episode:

  • We learned that generic parameters are only distributed if they are “naked”.
  • We disabled distribution by wrapping parameters in tuples: [T] extends [U].
  • We demonstrated how to generate unified generic arrays (A | B)[].
  • We built the legendary IsUnion<T> utility by comparing distributed evaluation against non-distributive tuple checks.

If you understood IsUnion, congratulations! You have mastered one of the most mechanically complex quirks in the TypeScript compiler.

In Episode 44: The infer Keyword, we will unlock the ability to declare variable names dynamically inside the extends clause!

typescript - This article is part of a series.
Part 43: This Article