- 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
TypeScript Generic Classes
In TypeScript, Generic Classes are a powerful tool that allows you to define a class structure without committing to a specific data type for its properties or methods until the class is actually instantiated. Think of them as templates: you define the logic once, and then you "fill in" the specific types (like string, number, or a custom Interface) when you create an object from that class.
What are Generic Classes?
A generic class is a class that can work with a variety of types while still maintaining full type safety. Without generics, you might be tempted to use the any type to handle different kinds of data. However, using any effectively turns off TypeScript's type checking, leading to potential runtime errors. Generics solve this by allowing the class to "capture" the type provided by the user, ensuring that the compiler knows exactly what kind of data is being handled at all times.
any when they want a class to be flexible. This defeats the purpose of TypeScript. Use a generic <T> instead to keep your code flexible AND safe.
Syntax of Generic Classes
The syntax involves placing a type parameter—usually represented by the letter T—inside angle brackets (<T>) immediately after the class name. This T then becomes a placeholder that you can use throughout the class body.
class MyComponent<T> {
content: T;
constructor(initialContent: T) {
this.content = initialContent;
}
getContent(): T {
return this.content;
}
}
<T>: This is the type parameter. WhileTis the convention (standing for "Type"), you could name itItemTypeorPayload.content: T: This property will strictly match whatever type is passed in during instantiation.getContent(): T: The method is guaranteed to return that same specific type.
T isn't clear enough, especially when using multiple generics (e.g., <TKey, TValue>).
Example of a Generic Class
1. Generic Box Class
Imagine you need a container to hold data, but you don't know yet if that data will be a user object, a simple number, or an array of strings. A generic Box class handles this perfectly:
class Box<T> {
private value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
setValue(newValue: T): void {
this.value = newValue;
}
}
// Usage with a number
const numberBox = new Box<number>(404);
console.log(numberBox.getValue()); // Output: 404
// Usage with a string
const stringBox = new Box<string>("Refactor Complete");
console.log(stringBox.getValue()); // Output: Refactor Complete
2. Generic Class with Multiple Type Parameters
Sometimes a single type isn't enough. For example, if you are building a custom data store or a key-value pair system, you might need two independent types.
class HttpResponse<Data, Status> {
payload: Data;
status: Status;
constructor(payload: Data, status: Status) {
this.payload = payload;
this.status = status;
}
}
// We can pass an object for Data and a number for Status
const response = new HttpResponse({ username: "Dev123" }, 200);
Using Constraints in Generic Classes
Sometimes, you want a class to be generic, but only for types that meet certain criteria. For instance, you might want to ensure that the type passed into your class has a length property. You can achieve this using the extends keyword.
interface HasLength {
length: number;
}
class ResourceInspector<T extends HasLength> {
resource: T;
constructor(resource: T) {
this.resource = resource;
}
logLength(): void {
// This is only safe because of the constraint 'extends HasLength'
console.log(`Length is: ${this.resource.length}`);
}
}
const text = new ResourceInspector("Hello World"); // Strings have .length
const list = new ResourceInspector([10, 20, 30]); // Arrays have .length
// const num = new ResourceInspector(100); // Error: numbers don't have .length
id property.
Generic Classes with Default Types
Just like default parameters in functions, you can provide a "fallback" type for your generic class. This makes the type argument optional when the class is instantiated.
class NotificationManager<T = string> {
message: T;
constructor(message: T) {
this.message = message;
}
}
// Defaults to string
const basic = new NotificationManager("System Update");
// Can still be overridden
const detailed = new NotificationManager({ code: 500, error: "Critical" });
Generic Classes with Methods
Generics are most useful when building data structures. Let's look at a practical Stack implementation. A stack is a "Last-In, First-Out" (LIFO) structure. By making it generic, we can have a stack of numbers, a stack of strings, or even a stack of UI components.
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 historyStack = new Stack<string>();
historyStack.push("/home");
historyStack.push("/settings");
console.log(historyStack.pop()); // Output: "/settings"
Summary
TypeScript Generic Classes are an essential part of a developer's toolkit for writing clean, DRY (Don't Repeat Yourself) code. By mastering them, you gain several advantages:
- Code Reusability: Write the logic once and apply it to any data type.
- Strong Type Safety: Avoid
anyand let TypeScript catch errors at compile time. - Predictable API: Constraints allow you to define exactly what your types are capable of (e.g., ensuring they have specific properties).
- Cleaner Syntax: Default types help reduce boilerplate when the most common use case is known.
When you find yourself writing two classes that do the exact same thing but handle different data types, it's a clear sign that you should be using a Generic Class instead.