Course

TypeScript Generics: Syntax, Usage, and Examples

Generics in TypeScript let you create flexible and reusable components that adapt to different types. Instead of hardcoding types, you can define functions, classes, and interfaces that handle multiple data types while maintaining type safety. This makes your code more scalable and prevents unnecessary duplication.

How to Use Generics in TypeScript

To define a generic, use angle brackets (<>) with a type parameter.

tsx
function identity<T>(arg: T): T { return arg; } console.log(identity<string>("Hello")); // Output: Hello console.log(identity<number>(42)); // Output: 42

Here, T acts as a placeholder for any type. When calling the function, you can specify the actual type, or TypeScript will infer it for you.

Generic Functions

A generic function adapts to different input types while maintaining type safety.

tsx
function getFirst<T>(arr: T[]): T { return arr[0]; } console.log(getFirst([10, 20, 30])); // Output: 10 console.log(getFirst(["a", "b", "c"])); // Output: "a"

Since getFirst uses a generic parameter, you don’t need to write separate functions for different array types.

Generic Interfaces

You can use generics in interfaces to define flexible data structures.

tsx
interface Box<T> { value: T; } let numberBox: Box<number> = { value: 123 }; let stringBox: Box<string> = { value: "Hello" }; console.log(numberBox.value); // Output: 123 console.log(stringBox.value); // Output: Hello

With a generic interface, you create structured objects that work with different types.

Generic Classes

You can also use generics in classes to build reusable components.

tsx
class Storage<T> { private data: T[] = []; addItem(item: T): void { this.data.push(item); } getItems(): T[] { return this.data; } } const numberStorage = new Storage<number>(); numberStorage.addItem(10); numberStorage.addItem(20); console.log(numberStorage.getItems()); // Output: [10, 20] const stringStorage = new Storage<string>(); stringStorage.addItem("Apple"); console.log(stringStorage.getItems()); // Output: ["Apple"]

This approach keeps your code type-safe while allowing different types of data.

When to Use Generics in TypeScript

Use generics when you need reusable, type-safe functions or data structures. They work well for utility functions like sorting or filtering, where the type may vary. If you’re building APIs, generics let you enforce strict type definitions while keeping things flexible.

Generics also help eliminate redundancy. Instead of writing multiple versions of a function for different types, you create one generic implementation that handles them all.

Examples of Generics in TypeScript

Adding Constraints to Generics

Sometimes, you need to ensure a generic type has certain properties. You can use constraints to enforce this.

tsx
interface Lengthy { length: number; } function getLength<T extends Lengthy>(item: T): number { return item.length; } console.log(getLength("Hello")); // Output: 5 console.log(getLength([1, 2, 3])); // Output: 3

By extending Lengthy, this function only accepts types with a length property.

Generic Arrow Functions

You can also use generics in arrow functions for cleaner syntax.

tsx
const reverseArray = <T>(items: T[]): T[] => items.reverse(); console.log(reverseArray([1, 2, 3])); // Output: [3, 2, 1] console.log(reverseArray(["a", "b", "c"])); // Output: ["c", "b", "a"]

This approach makes generic functions more concise and readable.

Working with Multiple Type Parameters

A function can use multiple generic parameters to handle different types.

tsx
function merge<T, U>(obj1: T, obj2: U): T & U { return { ...obj1, ...obj2 }; } const person = merge({ name: "Alice" }, { age: 30 }); console.log(person); // Output: { name: "Alice", age: 30 }

Here, merge takes two objects with different types and merges them into one.

Learn More About TypeScript Generics

Optional Generics

You can set a default type for generics, making them optional.

tsx
function wrapValue<T = string>(value: T): { wrapped: T } { return { wrapped: value }; } console.log(wrapValue(42)); // Output: { wrapped: 42 } console.log(wrapValue()); // Output: { wrapped: "" } (default type is string)

This makes your functions more adaptable without requiring explicit type arguments.

Generics with Object Types

Generics let you enforce type safety while working with objects.

tsx
function extractProperty<T, K extends keyof T>(obj: T, key: K) { return obj[key]; } const user = { name: "John", age: 28 }; console.log(extractProperty(user, "name")); // Output: John

This function ensures that key is a valid property of the object.

Generic Arrays

Generics help when filtering or modifying arrays while maintaining consistent types.

tsx
function filterItems<T>(arr: T[], predicate: (item: T) => boolean): T[] { return arr.filter(predicate); } console.log(filterItems([1, 2, 3, 4], (n) => n > 2)); // Output: [3, 4] console.log(filterItems(["apple", "banana"], (s) => s.includes("a"))); // Output: ["banana"]

This makes array operations more flexible without losing type safety.

Advanced Generic Constraints

You can enforce stricter constraints to refine generic behavior.

tsx
interface HasId { id: number; } function findById<T extends HasId>(items: T[], id: number): T | undefined { return items.find((item) => item.id === id); } const users = [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }]; console.log(findById(users, 2)); // Output: { id: 2, name: "Bob" }

This function ensures only objects with an id property can be used.

Checking Generic Types at Runtime

Since TypeScript types disappear at runtime, you can’t directly check a generic type. However, you can enforce constraints.

tsx
function isString<T>(value: T): boolean { return typeof value === "string"; } console.log(isString("hello")); // Output: true console.log(isString(100)); // Output: false

This function provides a way to check types without losing generic flexibility.