- TypeScript Tutorial
- TypeScript Home
- TypeScript Introduction
- TypeScript Setup
- TypeScript First Program
- TypeScript vs JavaScript
- TypeScript Data Types
- TypeScript Type Inference
- TypeScript Type Annotations
- TypeScript Interfaces
- TypeScript Enums
- TypeScript Type Aliases
- TypeScript Type Assertions
- TypeScript Variables
- TypeScript Functions
- TypeScript Functions
- TypeScript Optional Parameters
- TypeScript Default Parameters
- TypeScript Rest Parameters
- TypeScript Arrow Functions
- Classes and Objects
- Introduction to Classes
- Properties and Methods
- Access Modifiers
- Static Members
- Inheritance
- Abstract Classes
- Interfaces vs Classes
- Advanced Types
- TypeScript Union Types
- TypeScript Intersection Types
- TypeScript Literal Types
- TypeScript Nullable Types
- TypeScript Type Guards
- TypeScript Discriminated Unions
- TypeScript Index Signatures
- TypeScript Generics
- Introduction to Generics
- TypeScript Generic Functions
- TypeScript Generic Classes
- TypeScript Generic Constraints
- TypeScript Modules
- Introduction to Modules
- TypeScript Import and Export
- TypeScript Default Exports
- TypeScript Namespace
- Decorators
- Introduction to Decorators
- TypeScript Class Decorators
- TypeScript Method Decorators
- TypeScript Property Decorators
- TypeScript Parameter Decorators
- Configuration
- TypeScript tsconfig.json File
- TypeScript Compiler Options
- TypeScript Strict Mode
- TypeScript Watch Mode
Introduction to TypeScript Generics
Generics are often considered one of the most challenging parts of TypeScript, but they are also the most powerful. In essence, generics allow you to write code that is "type-agnostic"—it doesn't care about the specific data type it's handling right now, but it ensures that whatever type is used stays consistent throughout the execution.
By using generics, you can build components that are highly reusable while maintaining the strict type safety that makes TypeScript so valuable. Instead of using any and losing all the benefits of type checking, generics let you capture the type provided by the user and use it to enforce rules later.
What are Generics?
Generics act as placeholders for types. In a standard function, you define parameters for values; in a generic function, you also define parameters for the types of those values. When the code runs, TypeScript "fills in" these placeholders based on the actual data you provide.
Generics are identified by angle brackets (<T>). While T is the conventional shorthand for "Type," you can use any descriptive name, such as <UserType> or <Entity>.
any instead of Generics. While any allows any type, it "silences" the compiler. Generics, however, "remember" the type, providing full autocompletion and error checking.
Why Use Generics?
- Reusability: You can write a single logic block (like a data fetcher or a list sorter) that works for Users, Products, or Orders without rewriting the logic for each.
- Type Safety: TypeScript tracks the specific type through your logic. If you pass a
numberinto a generic function, TypeScript knows the return value is anumber. - Cleaner Code: It reduces "type casting" (using
as string) and prevents code duplication across your codebase.
Syntax of Generics
To define a generic, you place a type variable inside angle brackets immediately before the function's parentheses.
function identity<T>(value: T): T {
return value;
}
<T>: This declares a type parameterT. It tells TypeScript, "We're going to use a type here that we'll define later."value: T: This ensures the input matches our typeT.: T: This guarantees the function returns the exact same type that was passed in.
Example of Generics
1. Generic Function
A common real-world use case for a generic function is a utility that logs a value and returns it, or a function that wraps data in a consistent format.
function logAndReturn<T>(value: T): T {
console.log("Processing:", value);
return value;
}
const num = logAndReturn(100); // T is inferred as number
const str = logAndReturn("Hello"); // T is inferred as string
- In the first call, TypeScript sees
100and automatically "locks"Ttonumber. - In the second call,
Tbecomesstring. You don't have to manually tell TypeScript the type; it's smart enough to infer it.
2. Generic with Multiple Type Parameters
Sometimes you need to handle more than one type at once. You can use multiple placeholders by separating them with commas.
function mapPair<K, V>(key: K, value: V): string {
return `Key: ${key}, Value: ${value}`;
}
const result = mapPair(1, "Admin");
// K is number, V is string
This is extremely common in modern web development, such as when dealing with "Key-Value" pairs in a dictionary or state management system.
T, U, and V are standard conventions, don't be afraid to use descriptive names like <Data, Error> if it makes your code more readable for your team.
Generic Classes
Classes can also benefit from generics. A classic example is a State container or a Repository that handles database operations for different types of entities.
class DataStorage<T> {
private items: T[] = [];
addItem(item: T) {
this.items.push(item);
}
getItems(): T[] {
return this.items;
}
}
const textStorage = new DataStorage<string>();
textStorage.addItem("TypeScript"); // Valid
// textStorage.addItem(500); // Error: Argument of type 'number' is not assignable to 'string'
- The
DataStorageclass is now flexible. You can create a storage for strings, a storage for numbers, or even a storage for complex User objects.
T in static methods or properties.
Generic Interfaces
Interfaces often use generics to define the shape of API responses, which usually have a standard "wrapper" but different "data" payloads.
interface ApiResponse<T> {
status: number;
data: T;
message: string;
}
interface User {
id: number;
name: string;
}
const userResponse: ApiResponse<User> = {
status: 200,
data: { id: 1, name: "Jane Doe" },
message: "Success"
};
This approach allows you to reuse the ApiResponse interface for every single endpoint in your application while keeping the data property type-safe.
Constraints on Generics
Sometimes you want a function to be generic, but only for types that have certain properties. For example, if you want to access a .length property, you must ensure the type has it. You do this using the extends keyword.
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(item: T): void {
console.log(item.length);
}
logLength("Hello"); // Works (strings have length)
logLength([1, 2, 3]); // Works (arrays have length)
// logLength(123); // Error: number does not have a length property
Generic Defaults
Just like function parameters can have default values, generic type parameters can have default types. This is useful when you want a component to be generic but usually expect it to handle one specific type.
interface Config<T = string> {
value: T;
}
const defaultConfig: Config = { value: "Theme-Dark" }; // T defaults to string
const customConfig: Config<number> = { value: 404 }; // T is manually set to number
Summary
Generics in TypeScript are a cornerstone of professional-grade code. They allow you to build logic that is flexible enough to handle any data while remaining strict enough to prevent bugs before they happen. By mastering generics, you move from writing simple scripts to building scalable libraries and applications.
- Generic Function: Allows logic to adapt to the type of the arguments passed to it.
- Generic Classes and Interfaces: Create "blueprints" that can be customized with different data types.
- Constraints: Use the
extendskeyword to limit generics to types that meet specific requirements. - Default Types: Simplify your code by providing a fallback type for your generics.
As you continue your TypeScript journey, look for patterns in your code where you are repeating logic for different types—that is usually the perfect place to implement a Generic.