Implementing Distributive Types in TypeScript
TypeScript's powerful type system allows for elegant solutions to common programming challenges. Distributive types are a key feature that can significantly simplify how you handle unions of types. This challenge will guide you through understanding and implementing your own distributive types to transform union types in a controlled and predictable way.
Problem Description
Your task is to create a TypeScript utility type that takes a union of types and applies a transformation to each member of the union individually. This "distribution" is the core concept of distributive types. You need to design a generic type that accepts another type parameter representing the transformation to be applied. The utility type should then return a new union where each original type has been transformed.
Key Requirements:
- Generic Type: Create a generic type, let's call it
Distribute<T, U>, where:Trepresents the union of types to be distributed over.Urepresents the type or transformation to be applied to each member ofT.
- Distribution Logic: The
Distributetype should intelligently apply the transformationUto each constituent type within the unionT. - Preserve Union Structure: The final output should be a union of the transformed types.
Expected Behavior:
If T is a union like A | B | C, and U is some transformation mechanism, Distribute<T, U> should result in U<A> | U<B> | U<C>.
Edge Cases:
- What happens if
Tis not a union but a single type? - What happens if
Tisnever?
Examples
Example 1: Mapping a union of primitive types to their string representations.
// Input:
type OriginalUnion = string | number | boolean;
// Expected Output (after applying a hypothetical stringifying transformation):
type StringifiedUnion = Distribute<OriginalUnion, typeof String>; // Or a custom stringify type
// For illustration, let's imagine Distribute<OriginalUnion, typeof String> would resolve to:
// string | string | string which simplifies to string
// This example might be misleading if not carefully defined. Let's refine the transformation.
Let's use a more concrete transformation for clarity.
Example 1 (Refined): Wrapping each type in a specific object.
// Input:
type MyUnion = "user" | "admin" | "guest";
// Define a transformation that wraps a type in a property called 'role'
type WrapInRole<T> = { role: T };
// Expected Output:
// Distribute<MyUnion, WrapInRole> should resolve to:
// { role: "user" } | { role: "admin" } | { role: "guest" }
Example 2: Applying a conditional type transformation.
// Input:
type DataStatus = "loading" | "success" | "error" | undefined;
// Define a transformation that maps statuses to their data structures (or null for undefined)
type MapStatusToData<T> = T extends "success"
? { data: any }
: T extends "error"
? { error: string }
: T extends "loading"
? {}
: null;
// Expected Output:
// Distribute<DataStatus, MapStatusToData> should resolve to:
// {} | { data: any } | { error: string } | null
Example 3: Handling a non-union type.
// Input:
type SingleType = number;
// Using the same WrapInRole transformation from Example 1:
// Distribute<SingleType, WrapInRole> should resolve to:
// { role: number }
Constraints
- The
Distributetype must be implemented using TypeScript's built-in type system features (generics, conditional types, etc.). - The solution should be a single generic type definition.
- No runtime code is required; this is purely a type-level challenge.
- The solution should be efficient and not lead to excessive type computation or recursion depth.
Notes
The key to implementing distributive types lies in how TypeScript handles generic conditional types when the type parameter being distributed over is a union. Pay close attention to how T extends ... ? ... : ... behaves when T is a union. Consider creating a simple, focused transformation function (like WrapInRole or MapStatusToData) to test your Distribute type. Think about how never should be handled in your distribution.