Open In App

Mapping Distributive Types in TypeScript

Last Updated : 04 Oct, 2024
Comments
Improve
Suggest changes
Like Article
Like
Report

The TypeScript type system is considered to be quite expressive and, like all other languages, contains an interesting feature known as distributive types. The concept of distributive types allows one to perform operations of distributing conditional types over union types, which is extremely useful in many cases of advanced type manipulations when used with mapped types.

Distributive types offer a way to create more complex and better-adapting structures of types. This article will cover distributive types in TypeScript, their relations to union types, and how to utilize the mapping of distributive types.

Below are the following topics we are going to cover:

What Are Distributive Types?

In TypeScript, distributive types are those that happen when conditional types are invoked on union types, a conditional type is said to be distributed into a union of types, put differently, a conditional type offered for a union type applies to each union type element by distributing the condition across the members of the union and generating a fresh type.

Conditional types are distributive in this case it can be visualized as:

type Distributive<T> = T extends U ? X : Y;

If T is a union, then TS will apply this rule to every member of T, which is particularly beneficial for such actions as union-type filtering, transforming, or mapping.

Why Use Distributive Types?

Distributive types are interesting because they support type transformations relevant to each element of a union, these transformations are useful in situations such as:

  • Filtering members of a union: Extracting members of a union or cleansing it by removing members under some parameters.
  • Mapping unions to other types: Transforming each one of the union members to some particular type.
  • Performing complex type-level computations: Such as making conditional or recursive types that have their conditions based on the union member.

Understanding Mapping and Distributive Types

Mapping distributive types is a powerful combination of TypeScript’s conditional types and mapped types, Mapped types let you change the properties of an existing type to form a new type, and distributive types enable transforming the members of a union, the combination of these two concepts helps implement quite complex and safe constructs.

Examples of Mapping Distributive Types

Example 1: Excluding Members from a Union

Distributive types can narrow down a union by excluding specific members using conditional types. For example, ExcludeFromUnion<T, U> removes elements of type U from the union by setting them to never.

JavaScript
type ExcludeFromUnion<T, U> = T extends U ? never : T;

type Union = 'a' | 'b' | 'c';
type Excluded = ExcludeFromUnion<Union, 'a'>;

let result: Excluded;

result = 'b';
result = 'c'; 
// result = 'a'; 
// Error: Type '"a"' is not assignable to type 'Excluded'.

console.log(result); 

Output:

c

Example 2: Extracting Members from a Union

Similarly you can use distributive types to extract certain members from a union type, in this case ExtractFromUnion type extracts only members of the union that match U.

JavaScript
type ExtractFromUnion<T, U> = T extends U ? T : never;

type Union = 'a' | 'b' | 'c';
type Extracted = ExtractFromUnion<Union, 'a' | 'b'>;


let extractedResult: Extracted;

extractedResult = 'a'; 
extractedResult = 'b'; 
// extractedResult = 'c'; 
// Error: Type '"c"' is not assignable to type 'Extracted'.

console.log(extractedResult); 

Output:

b

Example 3: Mapping Union Types to New Types

You can use distributive conditional types to map each member of a union to a new structure. For example, WrapInObject<Union> wraps each member of Union in an object with a value property.

JavaScript
type WrapInObject<T> = { value: T };

type Union = 'a' | 'b' | 'c';
type WrappedUnion = WrapInObject<Union>;

const wrappedA: WrappedUnion = { value: 'a' };
const wrappedB: WrappedUnion = { value: 'b' };

console.log(wrappedA); 
console.log(wrappedB); 

Output:

{ value: 'a' }
{ value: 'b' }

Example 4: Deep Partial with Mapping Distributive Types

The DeepPartial<T> type recursively applies the Partial utility type to all properties of an object. It uses a distributive conditional type to check if each property is an object, applying itself to mark all properties, including nested ones, as optional.

JavaScript
type DeepPartial<T> = {
    [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

interface Example {
    a: number;
    b: string;
    c: {
        d: boolean;
        e: {
            f: string;
        };
    };
}

type PartialExample = DeepPartial<Example>;

const partial: PartialExample = {
    a: 42,
    c: {
        e: {
            f: 'hello',
        },
    },
};

console.log(partial); 

Output:

{ a: 42, c: { e: { f: 'hello' } } }

Limitations

  • Type Inference Limitations: In more complicated situations TypeScript may tend to have problems in type inference this means that it may lead to unpredictable behavior or even errors.
  • Performance: Distributive types can produce some degradation in compile time, especially when used on very complex and deeply nested types.
  • Recursive Depth: Recursive types (for example DeepPartial) can only include a certain amount of recursive levels, the errors occur for type recursion if this level is exceeded.

Best Practices

  • Use Utility Types: When it comes to these utility types in TypeScript, you usually don’t have to use so when creating types do not even think of it.
  • Avoid Overuse: Distributive Types are strong and powerful constructs but they also tend to complicate the understanding and maintenance of code, such type should be used whenever necessary.
  • Test with Multiple Scenarios: In case of different custom distributive types, input values at the time will be needed so that the designed types will behave as expected.

Conclusion

The ability to map distributive types in TypeScript is a potent feature that enables developers to modify and change a developer’s types during compilation, for Union Types and complex types, it means there are more type systems at work that allow for even greater expressiveness in the type system, many conventional programming languages have a looser coupling between data structures and associated algorithms, thus, mastery of mappable distributive conditional types is an important process in the evolution of advanced type-level programming that makes your code more robust yet flexible and maintainable.


Next Article
Article Tags :

Similar Reads