Why it's a bad idea to convert a union to a tuple type.

TypeScript offers a variety of useful type annotations to ensure type safety and improve code quality. Two of these annotations are union types and tuples, which are commonly used in TypeScript projects. Although unions and tuples have their own specific use cases, some developers have attempted to convert a union type into a tuple. However, this practice can have several negative consequences and is generally not recommended. In this article, we explain why converting a union type into a tuple is a bad idea.

In TypeScript, a union type represents a value that can be one of several types. In contrast, a tuple is a fixed ordered list of elements. While it might be tempting to convert a union type into a tuple, this approach can lead to various problems. This article explores the challenges of converting union types to tuples and presents alternative solutions.

The Problem of Determining the Resulting Type

Consider the following union type:

type Union = "string" | 0 | true;

When attempting to convert this union type into a tuple, the question arises: what should the resulting tuple look like? There are several possible options:

Option 1: All Possible Permutations

type TupleOption1 = 
| ["string", 0, true] 
| ["string", true, 0]
| [0, "string", true] 
| [0, true, "string"] 
| [true, "string", 0]
| [true, 0, "string"];

This option includes all possible permutations of the union types and ensures that every possible value is covered and used exactly once.

Option 2: A Specific Order

type TupleOption2 = ["string", 0, true];

Here, a specific order is defined, but it does not represent all possible combinations.

Option 3: A Tuple with Union Types as Elements

type TupleOption3 = [ "string" | 0 | true, "string" | 0 | true, "string" | 0 | true ];

In this option, the uniqueness and specific order of the elements are lost.

Conclusion: When converting a union type into a tuple, it's important to choose a tuple type that can represent all possible values of the original union type. TupleOption1 is the most flexible choice here, but it depends on the use case.

Problems with Converting Union Types to Tuples

Unpredictable Order of Elements

The order of a union is random; therefore, a specific order of elements in the resulting tuple cannot be guaranteed and may change over time, especially if new elements are added to the union type or TypeScript's internal implementation changes.

Unions in Primitive Types

Primitive types like boolean are themselves unions (true | false). When attempting to convert such types into tuples, unexpected results can occur.

type TupleFromBooleanUnion = UnionToTuple<boolean>; // [true, false] or [false, true], but NOT [boolean]

Similarly, with widened types like string | "a":

type TupleWithWidened = UnionToTuple<string | "a">; // [string], but NOT [string, "a"]

Combining these types:

type TupleWithWidenedAndBoolean = UnionToTuple<string | "a" | boolean>; // [string, false, true] or [...], but NOT [string, "a", boolean]

Compilation Issues and Performance

The TypeScript compiler can reach its recursion limit when trying to generate all possible permutations of a union type. This leads to performance problems, especially with large union types.

Limited Iteration Capabilities

TypeScript does not offer a native way to iterate over the elements of a union type—in a defined order. Workarounds often significantly impair compilation performance.

An Alternative Solution

Instead of trying to convert a union type into a tuple, it's often better to change the approach.

Problem Statement

Consider the following example:

type Contract = { name: string; age: number; isAlive: boolean; }; 
type ContractKey = keyof Contract; 

const contractKeys: ContractKey[] = ["name", "age", "isAlive"]; 

function isContractKey(key: string): key is ContractKey { 
   return contractKeys.includes(key as ContractKey); 
}

Problem: When a new key is added to the Contract type, there's no guarantee that contractKeys will be updated by a developer. This leads to a bug in isContractKey that TypeScript cannot catch.

Solution

Start by defining contractKeys and derive other types from it:

const contractKeys = ["name", "age", "isAlive"] as const; 
type ContractKey = typeof contractKeys[number]; 
function isContractKey(key: string): key is ContractKey { 
  return contractKeys.includes(key as ContractKey);
}

This approach ensures that all keys are consistent. If a new key is added, it only needs to be updated in one place, and TypeScript will show errors if the definitions do not match.

Conclusion

Converting a union type into a tuple in TypeScript presents several challenges, including unpredictable ordering, compilation issues, and performance limitations. Instead of trying to circumvent these problems, it is often more effective to rethink the approach and implement solutions that align better with TypeScript's strengths.

By carefully structuring the code and leveraging TypeScript's type system, you can create robust and maintainable applications without resorting to complex and error-prone type conversions.