Skip to main content

typescript-generics

md
---
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 named T
  • (arg: T) indicates that the argument should be of type T
  • : T indicates that the return value will be of the same type T

You can use this function with any type:

typescript
// 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

typescript
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

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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

  1. Collections and Data Structures: When implementing structures like lists, maps, etc.
  2. Component Props: For creating reusable React components that accept different prop types
  3. API Response Handling: For typing API responses with different data structures
  4. State Management: For creating type-safe stores or reducers
  5. Utility Functions: For functions that operate on various data types

Best Practices

  1. Use Descriptive Type Parameter Names

    • Use T for general types
    • Use more descriptive names for specific contexts: TItem, TKey, TValue
  2. Keep it Simple

    • Don't over-complicate with too many type parameters if not needed
    • Start simple and add complexity when required
  3. Use Constraints Appropriately

    • Apply constraints when you need to access specific properties/methods
    • Avoid overly restrictive constraints that limit reusability
  4. 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

  1. Create a generic useLocalStorage hook that can store and retrieve different data types.
  2. Build a generic pagination component that can work with different data structures.
  3. Implement a generic data fetching utility that includes caching capabilities.
  4. Create a typed state manager using generics and the Context API.
  5. 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! :)