DEV Community

Cover image for 10 Tips for Mastering TypeScript Generics
Akashdeep Patra
Akashdeep Patra

Posted on

10 Tips for Mastering TypeScript Generics

As a Senior software engineer, I've seen firsthand the transformative power of TypeScript's generics. They not only enforce type safety but also enhance code reuse and readability. However, diving into advanced generics can be daunting (Even highly skilled engineers have a hard time with it , trust me on that ). Here are ten tips to navigate these waters, each accompanied by a code snippet to illustrate the concept in action.

Contents

  1. Leveraging Conditional Types
  2. Using Type Inference in Generics
  3. Mapped Types with Generics
  4. Utility Types and Generics
  5. Generic Constraints
  6. Default Generic Types
  7. Advanced Pattern Matching with Conditional Types
  8. Type Guards and Differentiating Types
  9. Combining Generics with Enums for Type Safety
  10. Generic Type Aliases

1. Leveraging Conditional Types

Conditional types allow you to apply logic within the type system, enabling types that adapt based on the conditions met.

type IsString<T> = T extends string ? true : false;

// Usage
const isStringResult: IsString<string> = true; // true
const isStringResult2: IsString<number> = false; // false
Enter fullscreen mode Exit fullscreen mode

2. Using Type Inference in Generics

The infer keyword is a powerful feature within conditional types that allows you to infer a type for use within the rest of the type condition.

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

// Usage
function greet(name: string): string {
    return `Hello, ${name}!`;
}

type GreetReturnType = ReturnType<typeof greet>; // string
Enter fullscreen mode Exit fullscreen mode

3. Mapped Types with Generics

Mapped types enable you to create new types by transforming existing ones, iterating over their properties to apply modifications.

type ReadOnly<T> = { readonly [P in keyof T]: T[P] };

// Usage
interface Example {
    name: string;
    age: number;
}

type ReadOnlyExample = ReadOnly<Example>;
// ReadonlyExample: { readonly name: string; readonly age: number; }
Enter fullscreen mode Exit fullscreen mode

4. Utility Types and Generics

Utility types, provided by TypeScript, leverage generics to create common modifications of types, such as making all properties optional or read-only.

function update<T>(obj: T, changes: Partial<T>): T {
    return { ...obj, ...changes };
}

// Usage
interface User {
    name: string;
    age: number;
}

const user: User = { name: "Alice", age: 30 };
const updatedUser = update(user, { age: 31 });
Enter fullscreen mode Exit fullscreen mode

5. Generic Constraints

Constraints refine the types that can be used with generics, ensuring that they meet certain criteria or structures.

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

// Usage
const person = { name: "John", age: 30 };
const name = getProperty(person, "name"); // "John"
Enter fullscreen mode Exit fullscreen mode

6. Default Generic Types

Default types in generics provide a default type parameter, simplifying generic usage and enhancing API flexibility.

function createArray<T = number>(length: number, value: T): T[] {
    return new Array(length).fill(value);
}

// Usage
const numberArray = createArray(3, 0); // [0, 0, 0]
const stringArray = createArray<string>(3, "hello"); // ["hello", "hello", "hello"]
Enter fullscreen mode Exit fullscreen mode

7. Advanced Pattern Matching with Conditional Types

Enhancing conditional types with pattern matching allows for more precise type transformations and checks.

type Flatten<T> = T extends Array<infer U> ? U : T;

// Usage
type NestedArray = [number, [string, boolean], [object]];
type FlatArray = Flatten<NestedArray>; // number | string | boolean | object
Enter fullscreen mode Exit fullscreen mode

8. Type Guards and Differentiating Types

Type guards, especially when used with generics, allow for runtime type assertions, ensuring that your code remains type-safe even when dealing with unknown types.

function isString<T>(x: T): x is T extends string ? string : never {
    return typeof x === "string";
}

// Usage
const value: unknown = "Hello";
if (isString(value)) {
    console.log(value.toUpperCase()); // "HELLO"
}
Enter fullscreen mode Exit fullscreen mode

9. Combining Generics with Enums for Type Safety

Enums can be used alongside generics to create more restrictive and type-safe interfaces, particularly useful in function parameters and return types.

enum Status { New, InProgress, Done }

function setStatus<T extends Status>(status: T): void {
    console.log(status);
}

// Usage
setStatus(Status.New); // Status.New
Enter fullscreen mode Exit fullscreen mode

10. Generic Type Aliases

Type aliases with generics can define complex types in a more readable and maintainable way, facilitating code reuse and consistency.

type Container<T> = { value: T; timestamp: Date };

// Usage
const container: Container<number> = { value: 42, timestamp: new Date() };
Enter fullscreen mode Exit fullscreen mode

Conclusion

Advanced TypeScript generics unlock a myriad of possibilities for creating flexible, reusable, and type-safe code. By exploring these ten advanced tips, you're well on your way to mastering TypeScript generics, ready to tackle complex typing challenges with confidence but i'd still highly recommend the actual typescript docs .

Do comment on the article so that I can make this better and improve any mistakes I have made, thanks in advance.

Feel free to follow me on other platforms as well

Top comments (2)

Collapse
 
ahmadadibzad profile image
Ahmad Adibzad

Great post!

Collapse
 
ricardo_dev profile image
Ricardo Dev

excellent , thanks u