Sebastian Drozd

Understanding TypeScript's Advanced Generic Patterns

TypeScript's generics are a powerful feature that enable flexible and reusable code. While basic generics are easy to grasp, advanced patterns allow developers to create highly dynamic and type-safe applications. In this post, we'll explore some advanced generic patterns in TypeScript with practical examples.

1. Conditional Types

Conditional types allow us to create types that change based on a condition. These are useful for creating more flexible type constraints.

Example: Extracting the return type of a function

// Define a utility type to extract return type
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// Usage
function greet() {
  return "Hello, TypeScript!";
}

type GreetReturnType = ReturnType<typeof greet>; // string

The infer R keyword captures the return type of the function and assigns it to R, which is then returned as the type.


2. Mapped Types

Mapped types allow us to transform properties in an object type dynamically.

Example: Making all properties optional

type Partial<T> = {
  [K in keyof T]?: T[K];
};

interface User {
  id: number;
  name: string;
}

type OptionalUser = Partial<User>;
// Equivalent to:
// {
//   id?: number;
//   name?: string;
// }

By iterating over keyof T, we modify each property in T to be optional.


3. Variadic Tuple Types

Variadic tuples enable us to work with tuple types of varying lengths.

Example: Prepending a type to a tuple

type Prepend<T, U extends any[]> = [T, ...U];

type Result = Prepend<number, [string, boolean]>; 
// Result: [number, string, boolean]

This pattern is especially useful when working with functions that require dynamically extending arguments.


4. Recursive Type Inference

Recursive generics help model complex structures like JSON trees or nested arrays.

Example: Flattening a nested array

type Flatten<T> = T extends (infer U)[] ? Flatten<U> : T;

type NestedArray = number[][][];
type FlatArray = Flatten<NestedArray>; // number

This recursively unwraps nested arrays until it reaches the base type.


5. Template Literal Types

Template literal types enable dynamic string manipulation at the type level.

Example: Creating event name types

type EventType<T extends string> = `${T}Event`;

type ClickEvent = EventType<'Click'>;  // "ClickEvent"
type HoverEvent = EventType<'Hover'>;  // "HoverEvent"

This pattern is useful for defining consistent naming conventions across an application.


Conclusion

TypeScript’s advanced generic patterns provide powerful tools for creating flexible, type-safe applications. By leveraging conditional types, mapped types, variadic tuples, recursive inference, and template literal types, you can write more dynamic and robust TypeScript code.

Want to explore more? Try implementing your own utility types using these patterns and see how they can simplify your code!

Happy coding! 🚀