This chapter delves into generics in TypeScript, a powerful feature that enables developers to create reusable and flexible code components. Generics allow you to write functions, classes, and interfaces that work with a variety of data types while maintaining type safety.
Chapter Goal
- To understand what generics are and their purpose in TypeScript.
- To learn how to define and use generics in functions, classes, and interfaces.
- To explore practical applications of generics in real-world scenarios.
Key Characteristics for TypeScript Generics
- Type Safety: Ensures type checking while providing flexibility.
- Reusability: Allows for writing generic functions, classes, and interfaces.
- Type Parameters: Uses placeholders (like <T>) to represent types.
- Wide Compatibility: Works with any data type, reducing redundancy.
- Custom Constraints: Allows restriction of types using extends.
Basic Rules for TypeScript Generics
- Use <T> or similar placeholders to define generic types.
- Pass specific types when calling generic functions or creating instances.
- Use constraints to limit the types that can be passed as generics.
- Avoid overcomplicating generics with excessive parameters.
- Combine generics with utility types for added flexibility.
Best Practices
- Use meaningful names for type parameters (T, K, V, etc.).
- Combine generics with constraints to enforce specific type behavior.
- Avoid using any in generics to maintain type safety.
- Document the purpose of type parameters for better readability.
- Leverage built-in utility types (Partial, Readonly, etc.) with generics.
Syntax Table
Serial No | Component | Syntax Example | Description |
1 | Generic Function | function identity<T>(arg: T): T { return arg; } | A function that works with any data type. |
2 | Generic Class | class Box<T> { content: T; } | A class that holds a value of any type. |
3 | Generic Interface | interface Pair<K, V> { key: K; value: V; } | An interface with multiple generic type parameters. |
4 | Constraints | function logLength<T extends { length: number }>(arg: T): void {} | Restricts generics to types with a length property. |
5 | Default Type Parameters | function fetch<T = string>(url: string): T {} | Assigns a default type to a generic parameter. |
Syntax Explanation
1. Generic Function
What is a Generic Function
A function that can operate on arguments of different types, specified when the function is called.
Syntax
function identity<T>(arg: T): T {
return arg;
}
Detailed Explanation
- T is a type parameter that acts as a placeholder for the actual type.
- The identity function accepts an argument of type T and returns a value of the same type.
- Generics ensure the function maintains type safety while being reusable.
Example
console.log(identity<string>(‘Hello’)); // Output: Hello
console.log(identity<number>(42)); // Output: 42
Output
Hello
42
Notes
- Use generic functions for operations that apply to various data types.
Warnings
- Avoid using any in place of generics to retain type safety.
2. Generic Class
What is a Generic Class
A class that can work with properties or methods of different types.
Syntax
class Box<T> {
content: T;
constructor(value: T) {
this.content = value;
}
}
Detailed Explanation
- T represents a type parameter for the Box class.
- The content property and constructor parameter use the T type.
- Allows creating instances of Box for different types.
Example
const stringBox = new Box<string>(‘Books’);
console.log(stringBox.content); // Output: Books
const numberBox = new Box<number>(100);
console.log(numberBox.content); // Output: 100
Output
Books
100
Notes
- Use generic classes for containers or utilities that work with various types.
Warnings
- Ensure consistent type usage throughout the class to avoid confusion.
3. Generic Interface
What is a Generic Interface
An interface that defines a structure using generic type parameters.
Syntax
interface Pair<K, V> {
key: K;
value: V;
}
Detailed Explanation
- K and V represent key and value types, respectively.
- Allows defining data structures like key-value pairs with type safety.
Example
const pair: Pair<string, number> = { key: ‘Age’, value: 30 };
console.log(pair.key); // Output: Age
console.log(pair.value); // Output: 30
Output
Age
30
Notes
- Use generic interfaces for structured data with flexible types.
Warnings
- Avoid overly complex type parameters in interfaces.
4. Constraints
What are Constraints
Restrictions on generic type parameters to enforce specific type behaviors.
Syntax
function logLength<T extends { length: number }>(arg: T): void {
console.log(arg.length);
}
Detailed Explanation
- T extends { length: number } ensures T has a length property.
- Constraints improve type safety by limiting acceptable types.
Example
logLength(‘Hello’); // Output: 5
logLength([1, 2, 3]); // Output: 3
// logLength(42); // Error: Type ‘number’ does not have a ‘length’ property.
Output
5
3
Notes
- Use constraints to enforce specific characteristics of generic types.
Warnings
- Ensure constraints are meaningful and not overly restrictive.
5. Default Type Parameters
What are Default Type Parameters
Generic type parameters with default types when none are explicitly provided.
Syntax
function fetch<T = string>(url: string): T {
// Implementation
}
Detailed Explanation
- T = string sets string as the default type for T.
- If no type is specified, T defaults to string.
Example
function createArray<T = number>(length: number, value: T): T[] {
return Array(length).fill(value);
}
console.log(createArray(3, 42)); // Output: [42, 42, 42]
console.log(createArray(3, ‘Hi’)); // Output: [‘Hi’, ‘Hi’, ‘Hi’]
Output
[42, 42, 42]
[‘Hi’, ‘Hi’, ‘Hi’]
Notes
- Default type parameters simplify function calls when a common type is expected.
Warnings
- Avoid using defaults that may conflict with intended use cases.
Real-Life Project
Project Name
Generic Data Processor
Project Goal
Demonstrates how to use generics to create a data processing utility that works with various data types.
Code for This Project
class DataProcessor<T> {
private data: T[];
constructor(initialData: T[]) {
this.data = initialData;
}
addItem(item: T): void {
this.data.push(item);
}
getAll(): T[] {
return this.data;
}
}
const stringProcessor = new DataProcessor<string>(['Hello']);
stringProcessor.addItem('World');
console.log(stringProcessor.getAll()); // Output: ['Hello', 'World']
const numberProcessor = new DataProcessor<number>([1, 2, 3]);
numberProcessor.addItem(4);
console.log(numberProcessor.getAll()); // Output: [1, 2, 3, 4]
Save and Run
- Save the code in your development environment, such as Visual Studio Code, ensuring the file is named dataProcessor.ts.
- Compile the TypeScript code into JavaScript using the command tsc dataProcessor.ts in your terminal.
- Execute the resulting JavaScript file using the command node dataProcessor.js.
Expected Output
[‘Hello’, ‘World’]
[1, 2, 3, 4]
Insights
- Generics enhance reusability and type safety for data processing.
- Combining generics with constraints allows flexible yet controlled code.
- Real-world examples like data processors highlight the practical benefits of generics.
Key Takeaways
- Generics provide flexibility and type safety for reusable components.
- Use constraints to ensure meaningful type interactions.
- Default type parameters simplify usage in common scenarios.