TypeScript Basic Generics

Welcome to The Coding College! At The Coding College, we’re committed to providing clear, actionable, and type-safe solutions for developers. Today, we’ll delve into TypeScript Generics, a powerful feature that enhances the reusability and flexibility of your code.

What Are Generics in TypeScript?

Generics are a way to create reusable components that work with various data types while maintaining type safety. They act as placeholders for types, allowing you to write code that adapts to the type of data it operates on.

Why Use Generics?

  1. Type Safety: Generics prevent type-related runtime errors by ensuring types match at compile time.
  2. Code Reusability: Write reusable, type-agnostic functions, classes, and interfaces.
  3. Flexibility: Handle multiple types without compromising on type checking.

Syntax of Generics

Generics use type variables, commonly written as <T> (short for “Type”). The syntax is simple:

function functionName<T>(parameter: T): T {
    return parameter;
}

Examples of Basic Generics

1. Generic Functions

A generic function works with any type specified during its invocation.

Example: Identity Function

function identity<T>(value: T): T {
    return value;
}

console.log(identity<number>(42));       // Output: 42
console.log(identity<string>("Hello"));  // Output: Hello

Here, T is a placeholder that gets replaced with the actual type (number, string, etc.) during usage.

2. Generic Arrays

Generics can ensure type consistency in arrays.

Example: Working with Arrays

function getFirstElement<T>(arr: T[]): T {
    return arr[0];
}

console.log(getFirstElement<number>([1, 2, 3]));  // Output: 1
console.log(getFirstElement<string>(["a", "b"])); // Output: a

In this example, T[] ensures the array contains elements of the same type.

3. Using Multiple Type Variables

You can use multiple type variables for more complex scenarios.

Example: Key-Value Pairs

function mapToKeyValue<K, V>(key: K, value: V): [K, V] {
    return [key, value];
}

console.log(mapToKeyValue<string, number>("age", 30)); // Output: ["age", 30]

Here, K and V represent the key and value types, respectively.

Generics with Classes

Generics in classes allow you to define flexible and reusable structures.

Example: Generic Stack

class Stack<T> {
    private items: T[] = [];

    push(item: T): void {
        this.items.push(item);
    }

    pop(): T | undefined {
        return this.items.pop();
    }

    peek(): T | undefined {
        return this.items[this.items.length - 1];
    }
}

const numberStack = new Stack<number>();
numberStack.push(10);
numberStack.push(20);
console.log(numberStack.pop()); // Output: 20

Generics with Interfaces

Interfaces can also use generics to enforce type consistency.

Example: Generic Interface for API Response

interface ApiResponse<T> {
    data: T;
    status: number;
}

const userResponse: ApiResponse<{ name: string; age: number }> = {
    data: { name: "Alice", age: 25 },
    status: 200,
};

console.log(userResponse.data.name); // Output: Alice

Constraints in Generics

You can constrain a generic type to ensure it adheres to specific requirements.

Example: Using Constraints

function getLength<T extends { length: number }>(item: T): number {
    return item.length;
}

console.log(getLength("Hello")); // Output: 5
console.log(getLength([1, 2, 3])); // Output: 3
// console.log(getLength(42)); // Error: Type 'number' does not satisfy the constraint '{ length: number }'.

Here, <T extends { length: number }> ensures the type T must have a length property.

Default Types in Generics

TypeScript allows you to define default types for generics.

Example: Default Type

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

console.log(createArray(3, "A")); // Output: ["A", "A", "A"]
console.log(createArray<number>(3, 10)); // Output: [10, 10, 10]

If no type is specified, T defaults to string.

Best Practices for Generics

  1. Use Descriptive Names for Type Variables: Instead of <T>, use <Type> or <Key> for clarity.
  2. Avoid Overcomplicating: Keep generic functions and classes simple and focused.
  3. Use Constraints for Specific Use Cases: Apply constraints to narrow down acceptable types.
  4. Leverage Default Types: Provide default types where applicable to improve usability.
  5. Combine Generics with Type Guards: Validate the type before performing operations.

Real-World Applications of Generics

  1. Reusable Components: Generics make UI components reusable in frameworks like Angular.
  2. Strongly Typed APIs: Ensure consistent data structures in API responses.
  3. Utility Libraries: Build reusable utility functions and classes for common tasks.

Common Pitfalls

1. Overusing Generics

Generics can make code harder to read if overused or unnecessary.

// Avoid unnecessary generics
function logMessage<T>(message: T): void {
    console.log(message);
}

2. Ignoring Constraints

Without constraints, you risk runtime errors. Always constrain types if specific behavior is expected.

Conclusion

Generics are a powerful feature in TypeScript that allow you to write flexible, type-safe, and reusable code. From simple functions to complex classes, generics help maintain consistency and prevent errors, especially in large-scale applications.

Leave a Comment