Generics provide a way to create reusable components in TypeScript. They allow you to define a component that can work with a variety of data types rather than a single one. This makes your code more flexible and reusable.

Key Concepts

  1. Generic Functions: Functions that can operate on any data type.
  2. Generic Classes: Classes that can handle multiple data types.
  3. Generic Interfaces: Interfaces that can be used with different data types.
  4. Constraints: Restrictions on the types that can be used with generics.

Generic Functions

A generic function can work with any data type. Here's a simple example:

function identity<T>(arg: T): T {
    return arg;
}
  • T is a type variable that acts as a placeholder for the type that will be passed to the function.
  • When you call identity, you can specify the type explicitly or let TypeScript infer it.

Example

let output1 = identity<string>("Hello, TypeScript!"); // Explicit type
let output2 = identity(42); // TypeScript infers the type as number

Generic Classes

Generic classes can create objects that work with multiple data types. Here's an example:

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
  • T is a type variable that allows the class to work with any data type.
  • You can create instances of GenericNumber with different types.

Generic Interfaces

Generic interfaces can define the shape of objects that can work with multiple data types. Here's an example:

interface GenericIdentityFn<T> {
    (arg: T): T;
}

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

let myIdentity: GenericIdentityFn<number> = identity;
  • T is a type variable that allows the interface to work with any data type.
  • You can create instances of GenericIdentityFn with different types.

Constraints

Sometimes you want to restrict the types that can be used with generics. You can achieve this using constraints. Here's an example:

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length); // Now we know it has a .length property, so no error
    return arg;
}

loggingIdentity({ length: 10, value: 3 });
  • T extends Lengthwise means that T must have a length property.
  • This allows you to use generics with more specific types.

Practical Exercise

Exercise

Create a generic function merge that takes two objects and merges them into one. The function should return the merged object.

function merge<T, U>(obj1: T, obj2: U): T & U {
    return { ...obj1, ...obj2 };
}

// Test the function
const obj1 = { name: "John" };
const obj2 = { age: 30 };
const mergedObj = merge(obj1, obj2);
console.log(mergedObj); // Output: { name: "John", age: 30 }

Solution

function merge<T, U>(obj1: T, obj2: U): T & U {
    return { ...obj1, ...obj2 };
}

// Test the function
const obj1 = { name: "John" };
const obj2 = { age: 30 };
const mergedObj = merge(obj1, obj2);
console.log(mergedObj); // Output: { name: "John", age: 30 }

Common Mistakes and Tips

  • Forgetting to Specify the Type: If you don't specify the type, TypeScript will try to infer it. This can sometimes lead to unexpected results.
  • Overusing Generics: While generics are powerful, overusing them can make your code harder to read and maintain. Use them judiciously.
  • Ignoring Constraints: Constraints help ensure that your generics work with the types you expect. Use them to avoid runtime errors.

Conclusion

Generics are a powerful feature in TypeScript that allow you to create flexible and reusable components. By understanding how to use generic functions, classes, interfaces, and constraints, you can write more robust and maintainable code. In the next section, we will explore Type Aliases, which provide another way to define custom types in TypeScript.

© Copyright 2024. All rights reserved