Technology  /  TypeScript

๐Ÿ”ท TypeScript 16 guides ยท updated 2026

Type erasure, structural typing, generics, and the type-level programming toolkit โ€” TypeScript taught the way it actually behaves at compile time and at runtime.

Generics in TypeScript: Writing Reusable, Type-Safe Code

The moment you write the same function twice with only the type changed โ€” once for string[], once for number[] โ€” is the moment you need a generic. Generics let a function, class, or type parameter over a type the same way a regular parameter parameters over a value: you donโ€™t fix the type at the point you write the code, you fix it at the point you use it, and TypeScript checks everything consistently either way.


The Problem Generics Solve

// Without generics โ€” either lose type safety, or duplicate the function per type
function firstStringElement(arr: string[]): string {
return arr[0];
}
function firstNumberElement(arr: number[]): number {
return arr[0];
}
// Or give up type safety entirely:
function firstElementAny(arr: any[]): any {
return arr[0]; // works for any array, but you get zero type information back
}
// With a generic
function firstElement<T>(arr: T[]): T {
return arr[0];
}
const firstNum = firstElement([1, 2, 3]); // inferred: T is number, return type is number
const firstStr = firstElement(["a", "b", "c"]); // inferred: T is string, return type is string

<T> declares a type parameter โ€” a placeholder that gets filled in with a concrete type at each call site, inferred automatically from the argument you actually pass. Unlike the any version, the generic version preserves the connection between input and output: pass in a number[], get back a number, fully type-checked, with zero duplication.

T = number

T = string

T = User

firstElement([1,2,3])

returns number

firstElement(['a','b'])

returns string

firstElement([user1, user2])

returns User


Generic Functions With Multiple Type Parameters

function pair<A, B>(first: A, second: B): [A, B] {
return [first, second];
}
pair("age", 30); // [string, number]
pair(true, [1, 2, 3]); // [boolean, number[]]
// A practical example: a type-safe object property getter
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 30 };
getProperty(user, "name"); // typed as string
getProperty(user, "age"); // typed as number
getProperty(user, "email"); // Error: "email" is not a key of { name: string; age: number }

K extends keyof T is worth pausing on โ€” keyof T produces a union of Tโ€™s literal property names ("name" | "age" here), and constraining K to extend that union means only real keys of T are ever accepted. T[K] then looks up the exact type of that specific property. This combination โ€” generics plus keyof plus indexed access โ€” is how TypeScript expresses โ€œgive me back exactly the right type for whichever key was actually requested,โ€ something a non-generic signature simply cannot do.


Generic Interfaces and Type Aliases

interface Box<T> {
value: T;
map<U>(fn: (value: T) => U): Box<U>;
}
function createBox<T>(value: T): Box<T> {
return {
value,
map(fn) {
return createBox(fn(value));
},
};
}
const numberBox = createBox(5);
const stringBox = numberBox.map(n => n.toString()); // Box<string>
type ApiResult<T> =
| { success: true; data: T }
| { success: false; error: string };
function unwrap<T>(result: ApiResult<T>): T {
if (!result.success) throw new Error(result.error);
return result.data;
}

Generic interfaces and type aliases are how reusable data shapes (a โ€œbox holding some value,โ€ a โ€œresult that succeeded or failed with some payloadโ€) get written once and instantiated for every concrete type your application actually uses, without losing type information at any point.


Generic Classes

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];
}
get size(): number {
return this.items.length;
}
}
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
numberStack.pop(); // number | undefined
const userStack = new Stack<User>();
userStack.push({ name: "Alice", age: 30 });

A generic class fixes its type parameter once, at construction (new Stack<number>()), and every method on that instance uses the same concrete type consistently thereafter โ€” numberStack.push("hello") fails to compile because T was already fixed to number for that specific instance.


Generic Constraints

// Without a constraint, T could be anything โ€” including something with no .length property
function logLength<T>(item: T) {
console.log(item.length); // Error: Property 'length' does not exist on type 'T'
}
// With a constraint, T is guaranteed to have .length
function logLength<T extends { length: number }>(item: T) {
console.log(item.length); // fine
}
logLength("hello"); // strings have .length
logLength([1, 2, 3]); // arrays have .length
logLength({ length: 5 }); // any object shape with .length works
logLength(42); // Error: number doesn't have .length

A constraint (extends) narrows what a type parameter is allowed to be, in exchange for letting you actually use properties/methods the constraint guarantees exist. Without a constraint, T could be absolutely anything, so TypeScript refuses to let you assume it has any particular property at all โ€” this is the same โ€œonly what every member guaranteesโ€ rule from union narrowing, applied to generics instead.

// A common, practical constraint: requiring an id property for anything "findable"
interface HasId {
id: string;
}
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
findById([{ id: "1", name: "Alice" }, { id: "2", name: "Bob" }], "1");

Default Type Parameters

interface ApiResponse<T = unknown> {
data: T;
status: number;
}
function fetchGeneric<T = unknown>(url: string): Promise<ApiResponse<T>> {
return fetch(url).then(res => res.json());
}
const response1: ApiResponse = { data: "anything", status: 200 }; // T defaults to unknown
const response2: ApiResponse<User> = { data: someUser, status: 200 }; // T explicitly set to User

A default type parameter (T = unknown) makes the generic usable without specifying T at all, falling back to a sensible default rather than forcing every call site to write out the type argument explicitly โ€” useful for generic utility types and functions where most callers will specify the type but a reasonable fallback still makes sense for the rest.


Generic Constraints With Multiple Parameters

function merge<T extends object, U extends object>(first: T, second: U): T & U {
return { ...first, ...second };
}
const merged = merge({ name: "Alice" }, { age: 30 });
// merged: { name: string } & { age: number }
console.log(merged.name, merged.age);

Constraining both T and U to object (rather than leaving them fully open) prevents nonsensical calls like merge("hello", 5), where spreading a string or number wouldnโ€™t make sense โ€” the constraint documents and enforces the actual requirement the functionโ€™s implementation depends on.


Where Generics Show Up in Code You Already Use

// Array methods are generic
const numbers: number[] = [1, 2, 3];
const doubled = numbers.map<number>(n => n * 2); // Array.prototype.map<U>(fn: (value: T) => U): U[]
// Promise is generic
const userPromise: Promise<User> = fetchUser();
// Map and Set are generic
const cache: Map<string, User> = new Map();
const seenIds: Set<string> = new Set();
// React's useState is generic (if you've used React)
// const [count, setCount] = useState<number>(0);

Recognizing that these everyday types are themselves generic is often the moment generics stop feeling abstract โ€” youโ€™ve been using them constantly; writing your own is the same mechanism applied to your own reusable code.


A Realistic End-to-End Example

interface Repository<T extends { id: string }> {
findById(id: string): T | undefined;
save(item: T): void;
delete(id: string): void;
}
class InMemoryRepository<T extends { id: string }> implements Repository<T> {
private items = new Map<string, T>();
findById(id: string): T | undefined {
return this.items.get(id);
}
save(item: T): void {
this.items.set(item.id, item);
}
delete(id: string): void {
this.items.delete(id);
}
}
interface Product {
id: string;
name: string;
price: number;
}
const productRepo = new InMemoryRepository<Product>();
productRepo.save({ id: "p1", name: "Widget", price: 9.99 });
const found = productRepo.findById("p1"); // Product | undefined, fully typed

This is the pattern generics exist for at scale: one Repository<T> interface and one InMemoryRepository<T> implementation, reused for Product, User, Order, or anything else with an id, instead of writing a near-identical repository class per entity type.


A .tsx Gotcha Worth Knowing Early

// In a plain .ts file, this generic arrow function is fine:
const firstElement = <T,>(arr: T[]): T => arr[0];
// In a .tsx file, <T> alone is ambiguous with JSX syntax and fails to parse:
const firstElement = <T>(arr: T[]): T => arr[0]; // Error โ€” parser reads <T> as a JSX tag
// Fix: add a trailing comma to disambiguate from JSX
const firstElement = <T,>(arr: T[]): T => arr[0];
// Or avoid the ambiguity entirely with a named function declaration
function firstElement<T>(arr: T[]): T {
return arr[0];
}

This is a genuinely confusing error the first time you hit it in a React + TypeScript project: .tsx files parse <T> as the start of a JSX element by default, since both use angle brackets. The trailing comma (<T,>) is enough to tell the parser โ€œthis is a generic type parameter list, not JSXโ€ โ€” many teams simply prefer named function declarations for generic utilities in .tsx files specifically to sidestep the ambiguity altogether.

Frequently Asked Questions

Do I need to always specify the type parameter explicitly, like firstElement<number>([1,2,3])? No โ€” in almost every case TypeScript infers T from the arguments you pass, which is why most of the examples above never write <T> explicitly at the call site. Explicit type arguments are mainly needed when thereโ€™s nothing for TypeScript to infer from (an empty array literal with no other context, for instance) or when you want to widen/narrow the inferred type deliberately.

Whatโ€™s the difference between T in a generic and any? any disables checking entirely and loses all information about what the value actually is. A generic T preserves a specific, consistent type across an entire function call or class instance โ€” pass in a number[], and every place T is used inside that call is checked as number, unlike any, where nothing is checked at all.

Why does logLength<T>(item: T) fail without a constraint, when JavaScript would happily let me call .length on anything? JavaScript would happily try โ€” and then either work or throw undefined is not a function at runtime depending on whether the value actually has that property. TypeScriptโ€™s job is refusing operations it canโ€™t prove are safe at compile time; an unconstrained T could be a number, which has no .length, so the compiler has no basis to allow the call.

Can generic type parameters have default values based on other type parameters? Yes, though itโ€™s a more advanced pattern: interface Pair<T, U = T> lets Pair<string> mean Pair<string, string> while Pair<string, number> still works when both need to differ โ€” useful for APIs where the two types are related in the common case but genuinely independent in less common ones.

How many type parameters is too many? Thereโ€™s no hard rule, but past two or three, a generic signature usually becomes hard to read and to call correctly. If youโ€™re reaching for four or more type parameters, consider whether some of them belong grouped into a single generic options object type instead.

Why did adding a generic to my React component break in a .tsx file? Almost certainly the JSX-ambiguity issue above โ€” <T> alone at the start of an arrow functionโ€™s type parameter list is indistinguishable from an opening JSX tag in .tsx. Add the trailing comma or switch to a function declaration.


Whatโ€™s Next

Generics are what make TypeScriptโ€™s type system genuinely expressive rather than just a static type-checker bolted onto JavaScript โ€” theyโ€™re the foundation for the utility types built into the language itself. Next: Utility Types, where youโ€™ll see Partial<T>, Pick<T, K>, Omit<T, K>, and the rest of TypeScriptโ€™s built-in generic helpers, all built from exactly the mechanisms covered here.