This repository provides simple examples to demonstrate the use of generic types in TypeScript.
Generics allow you to create reusable components that can work with a variety of types rather than a single one. This enhances flexibility and type safety.
A common use case for generics is to create a wrapper for API responses. The structure of the response is often the same (e.g., it has a data field, a success flag, and an optional error message), but the type of the data can vary.
In src/api-response.ts, we define a generic ApiResponse<T> type:
type ApiResponse<T> = {
data: T;
success: boolean;
error?: string;
};The T is a placeholder for a type that will be provided when we use ApiResponse.
We can then use this generic type for different kinds of data.
type User = { id: string; name: string };
const userResponse: ApiResponse<User> = {
data: {
id: "1",
name: "Putri",
},
success: true,
};Here, T is replaced with User.
const stringResponse: ApiResponse<string> = {
data: "Hello world!",
success: true,
};Here, T is replaced with string.
Generics are also incredibly useful for functions. You can create a function that operates on or returns a value whose type is not known ahead of time.
In src/api-client.ts, we define a generic fetchJSON<T> function to fetch and parse data from a URL:
async function fetchJSON<T>(url: string): Promise<T> {
const res = await axios.get(url);
if (!res.status.toString().startsWith("2")) {
throw new Error(res.statusText);
}
return res.data;
}Here, T represents the expected type of the data in the response body. The function promises to return a value of that type.
We can use this function to fetch different types of data by specifying the type when we call it.
type Todo = {
userId: number;
id: number;
title: string;
completed: boolean;
};
// We expect an array of Todo objects
const todos = await fetchJSON<Todo[]>(
"https://jsonplaceholder.typicode.com/todos"
);In this case, we tell fetchJSON that T should be Todo[], and TypeScript will ensure that the todos constant is correctly typed as an array of Todo objects.
We can also create generic classes. A powerful feature to use with generic classes (and functions) is constraints. Constraints allow us to limit the kinds of types that can be used as a type argument.
In src/base-repository.ts, we define a generic InMemoryRepository<T> that can work with any entity type, as long as that entity has an id property.
First, we define a generic Repository interface:
interface Repository<T> {
findById(id: string): Promise<T | null>;
findAll(): Promise<T[]>;
create(item: T): Promise<T>;
update(id: string, item: Partial<T>): Promise<T>;
delete(id: string): Promise<boolean>;
}Then, we implement this with a generic class that uses a constraint.
class InMemoryRepository<T extends { id: string }> implements Repository<T> {
private store: Map<string, T> = new Map();
// ... implementation ...
async create(item: T): Promise<T> {
// We can safely access item.id because of the constraint
this.store.set(item.id, item);
return item;
}
// ... other methods
}The key part is <T extends { id:string }>. This tells TypeScript that T can be any type, as long as it has a property id of type string. This allows us to safely use item.id inside our class methods.
We can now create repositories for different entities that match the constraint.
// Entities that satisfy the constraint { id: string }
type User = {
id: string;
name: string;
};
type Product = {
id: string;
name: string;
price: number;
};
const userRepository = new InMemoryRepository<User>();
const productRepository = new InMemoryRepository<Product>();- Install dependencies:
bun install
- Run the examples:
# Run the generic type example bun src/api-response.ts # Run the generic function example bun src/api-client.ts # Run the generic class example bun src/base-repository.ts
This will execute the main functions in each file and log the results to the console.