typescript-generics
---
title: TypeScript Generics
description: Learn how to use TypeScript generics to write flexible, reusable, and type-safe code that works with different data types while maintaining type safety.
---
# TypeScript Generics
TypeScript generics are powerful features that allow you to create reusable components that work with a variety of types rather than a single one. They enable you to build flexible, type-safe code without sacrificing the benefits of TypeScript's static typing.
## Introduction to Generics
When building applications, especially with Next.js, you'll often need to create functions, classes, or interfaces that should work across many data types. Without generics, you would need to:
1. Use the `any` type (losing type safety)
2. Create duplicate code for each type (violating DRY principles)
Generics solve this by letting you define type variables that capture the types provided by users, which you can then use throughout your code.
## Basic Generic Syntax
The syntax for generics uses angle brackets (`<>`) with type parameters inside:
```typescript
function identity<T>(arg: T): T {
return arg;
}
In this example:
<T>
declares a type parameter namedT
(arg: T)
indicates that the argument should be of typeT
: T
indicates that the return value will be of the same typeT
You can use this function with any type:
// Explicitly specify the type
let output1 = identity<string>("Hello, TypeScript!");
// Type inference - TypeScript infers the type automatically
let output2 = identity(42);
console.log(output1); // "Hello, TypeScript!"
console.log(output2); // 42
Generic Interfaces and Classes
You can also create generic interfaces and classes:
Generic Interface
interface Container<T> {
value: T;
getValue(): T;
}
const stringContainer: Container<string> = {
value: "Hello World",
getValue() {
return this.value;
}
};
console.log(stringContainer.getValue()); // "Hello World"
Generic Class
class Box<T> {
private content: T;
constructor(value: T) {
this.content = value;
}
getContent(): T {
return this.content;
}
}
const numberBox = new Box<number>(123);
console.log(numberBox.getContent()); // 123
const stringBox = new Box("TypeScript");
console.log(stringBox.getContent()); // "TypeScript"
Generic Constraints
Sometimes you need to restrict the types that can be used with your generics. You can do this using the extends
keyword:
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(arg: T): T {
console.log(`Length: ${arg.length}`);
return arg;
}
// These work because they have a length property
logLength("Hello"); // Length: 5
logLength([1, 2, 3]); // Length: 3
logLength({ length: 10 }); // Length: 10
// This would cause a compilation error
// logLength(123);
Multiple Type Parameters
You can use more than one type parameter in your generics:
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const p1 = pair<string, number>("age", 30);
const p2 = pair(true, "yes"); // Type inference: [boolean, string]
console.log(p1); // ["age", 30]
console.log(p2); // [true, "yes"]
Generic Type Defaults
Just like function parameters can have defaults, type parameters can have defaults too:
interface ApiResponse<T = any> {
data: T;
status: number;
message: string;
}
// Using the default type
const genericResponse: ApiResponse = {
data: { anything: "goes" },
status: 200,
message: "Success"
};
// Specifying a type
interface User {
id: number;
name: string;
}
const userResponse: ApiResponse<User> = {
data: { id: 1, name: "John Doe" },
status: 200,
message: "User retrieved successfully"
};
Practical Examples in Next.js Context
Generic API Handler
In Next.js API routes, you might want to create a reusable handler:
type ApiHandler<T> = (
req: NextApiRequest,
res: NextApiResponse<T | { error: string }>
) => Promise<void>;
function createApiHandler<T>(
handler: (req: NextApiRequest) => Promise<T>
): ApiHandler<T> {
return async (req, res) => {
try {
const result = await handler(req);
res.status(200).json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
};
}
// Usage in an API route
export default createApiHandler<User[]>(async (req) => {
// Fetch users from database
return users;
});
Generic Data Fetching Hook
When building Next.js applications, you often need to fetch data. A generic hook can help:
import { useState, useEffect } from 'react';
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (e) {
setError(e instanceof Error ? e : new Error(String(e)));
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
// Usage
interface Post {
id: number;
title: string;
body: string;
}
function PostsList() {
const { data, loading, error } = useFetch<Post[]>('/api/posts');
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{data?.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
Generic Context Provider
Creating type-safe context providers in React/Next.js:
import { createContext, useState, ReactNode } from 'react';
interface ContextProviderProps<T> {
initialValue: T;
children: ReactNode;
}
export function createTypedContext<T>() {
const TypedContext = createContext<{
state: T;
setState: React.Dispatch<React.SetStateAction<T>>;
} | undefined>(undefined);
const TypedProvider = ({ initialValue, children }: ContextProviderProps<T>) => {
const [state, setState] = useState<T>(initialValue);
return (
<TypedContext.Provider value={{ state, setState }}>
{children}
</TypedContext.Provider>
);
};
const useTypedContext = () => {
const context = React.useContext(TypedContext);
if (!context) {
throw new Error('useTypedContext must be used within a TypedProvider');
}
return context;
};
return { Provider: TypedProvider, useContext: useTypedContext };
}
// Usage
interface ThemeState {
isDarkMode: boolean;
primaryColor: string;
}
const { Provider: ThemeProvider, useContext: useTheme } = createTypedContext<ThemeState>();
// In your app
function App() {
return (
<ThemeProvider initialValue={{ isDarkMode: false, primaryColor: '#007bff' }}>
<YourComponent />
</ThemeProvider>
);
}
function YourComponent() {
const { state, setState } = useTheme();
return (
<div>
<p>Current theme: {state.isDarkMode ? 'Dark' : 'Light'}</p>
<button onClick={() => setState(prev => ({ ...prev, isDarkMode: !prev.isDarkMode }))}>
Toggle theme
</button>
</div>
);
}
Common Use Cases for Generics
- Collections and Data Structures: When implementing structures like lists, maps, etc.
- Component Props: For creating reusable React components that accept different prop types
- API Response Handling: For typing API responses with different data structures
- State Management: For creating type-safe stores or reducers
- Utility Functions: For functions that operate on various data types
Best Practices
-
Use Descriptive Type Parameter Names
- Use
T
for general types - Use more descriptive names for specific contexts:
TItem
,TKey
,TValue
- Use
-
Keep it Simple
- Don't over-complicate with too many type parameters if not needed
- Start simple and add complexity when required
-
Use Constraints Appropriately
- Apply constraints when you need to access specific properties/methods
- Avoid overly restrictive constraints that limit reusability
-
Consider Type Inference
- Let TypeScript infer types when possible for cleaner code
- Provide explicit types when inference isn't clear or for documentation
Summary
TypeScript generics are a powerful feature that allows you to write flexible, reusable, and type-safe code. They are especially useful in Next.js applications for creating:
- Type-safe API handlers
- Reusable data fetching hooks
- Type-safe context providers
- Generic utility functions
By using generics, you can maintain strong typing while avoiding code duplication, making your Next.js applications more maintainable and robust.
Additional Resources
Exercises
- Create a generic
useLocalStorage
hook that can store and retrieve different data types. - Build a generic pagination component that can work with different data structures.
- Implement a generic data fetching utility that includes caching capabilities.
- Create a typed state manager using generics and the Context API.
- Build a form handling utility that can validate different input types using generics.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)