How to Transform Union Type to Tuple Type in TypeScript

Updated: January 8, 2024 By: Guest Contributor Post a comment

Introduction

TypeScript, as a superset of JavaScript, enhances the language by adding static types. Types provide a way to describe the shape of an object, providing better documentation, and allowing TypeScript to validate that your code is working correctly. This tutorial will explore how to transform union types into tuple types—a technique that can streamline your type definitions and code readibility in TypeScript.

Understanding Union Types

In TypeScript, a union type is a type that can be one of several types. We define them using the pipe (|) syntax. Union types are particularly useful when a value can accept multiple types and we want to maintain that flexibility.

// Example of a simple union type
type StringOrNumber = string | number;

// We can use this type for variables that may hold either a string or a number
const value: StringOrNumber = 'Hello World'; // OK
const anotherValue: StringOrNumber = 42; // OK

From Union to Tuple

Transforming a union to a tuple type can be desired when one aims to represent an array where the type of a fixed number of elements is known, but they are not necessarily of the same type. A tuple type allows you to express an array where the type of a certain index is known.

// Example of a tuple type
let userDetails: [number, string] = [1, 'John Doe'];

The Transformation Challenge

The challenge with transforming a union to a tuple type is that TypeScript’s type system does not natively support such transformation. Hence, we rely on advanced types and generics.

Step 1: Conditional Type Checking

First, we need to understand conditional types which allow us to work with types in a more dynamic way.

// Example of a conditional type
type Check = Type extends string ? 'String' : 'Not a String';

// Usage of our Check conditional type
type IsString = Check; // 'String'
type IsNotString = Check; // 'Not a String'

Step 2: Distributive Conditional Types

TypeScript has a feature known as distributive conditional types that can distribute over union types. This feature allows us to perform operations on each member of a union type separately. This is the cornerstone of converting a union to a tuple since it allows us to iterate over the elements of the union.

// Example of distributive conditional type
type ToArray = Type extends any ? Type[] : never;

// Distributing over a union type
type Result = ToArray; // string[] | number[]

Step 3: Infer within Conditional Types

The infer keyword in TypeScript permits us to infer a type within the branches of conditional types. Combined with distributive properties, we can construct types in a more flexible manner.

// Example of inferring within conditional types
type ElementType = T extends (infer U)[] ? U : never;

// Inferring element type of an array
const array = [1, 'string'];
type InferredElementType = ElementType; // number | string

Step 4: Building the Union to Tuple Transformation

Now armed with the knowledge of conditional types and the infer keyword, we can build complex type transformations to convert unions into tuple types. Let’s do this stepwise.

// Using recursive conditional types to build a tuple from a union
type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
type LastOfUnion = UnionToIntersection U : never> extends () => infer L ? L : never;
type Push = [...T, V];
type UnionToTuple<U, T extends any[] = []> = {
  '1': T,
  '0': UnionToTuple<Exclude<U, LastOfUnion>, Push<T, LastOfUnion>>
}[U extends any ? ('0') : ('1')];

// Transforming a union to tuple
// Original Union type
type ShapeTypes = 'circle' | 'square' | 'triangle';
// Converted Tuple type
type ShapeTuple = UnionToTuple; // ['circle', 'square', 'triangle']

Conclusion

Although transforming union types to tuple types in TypeScript is not inherently built into the language, with advanced techniques using generics and conditional types, such transformations are feasible. Through this tutorial, we’ve demystified the process using a combinaton of distributive conditional types, the infer keyword, and recursive type aliases. This pattern is invaluable for TypeScript developers seeking to enforce strict type constraints on arrays and enhances the overall safety and expressiveness of the language.