Introduction to Generics in TypeScript


Why Generics Exist
Consider a simple function that returns the first element of an array:
function first(arr: number[]): number {
return arr[0];
}Works fine for numbers. But what about strings? You write another version:
function firstString(arr: string[]): string {
return arr[0];
}And another for booleans. And another for any custom type you create. This is code duplication — the logic is identical, only the type changes. The standard solution is to reach for any:
function first(arr: any[]): any {
return arr[0];
}But any throws away all type information. Call first([1, 2, 3]) and the return type is any, not number. TypeScript can't help you use the result correctly because it has no idea what type it is.
Generics solve exactly this problem. They let you write the function once and preserve type information across the call:
function first<T>(arr: T[]): T {
return arr[0];
}
const n = first([1, 2, 3]); // TypeScript knows: n is number
const s = first(["a", "b", "c"]); // TypeScript knows: s is stringThe <T> is a type parameter — a placeholder that TypeScript fills in based on what you pass. Write the logic once; get type safety for every type.
Generics Are Not Templates
If you come from C++ or Java, TypeScript generics feel familiar but work differently. TypeScript generics are erased at compile time — there's no runtime specialization. The type checking happens entirely at compile time, and the compiled JavaScript is just a regular function. Generics are a purely compile-time tool.
Your First Generic Function
The angle-bracket syntax <T> after a function name declares a type parameter. By convention, single-letter uppercase names are used — T for "Type", K for "Key", V for "Value", E for "Element". For more specific parameters, descriptive names like TItem or TResult are also common.
Here is the pattern:
function identity<T>(value: T): T {
return value;
}identity is the simplest possible generic — it takes a value and returns the same value, preserving its type exactly. This is the "hello world" of generics:
identity(42); // returns 42, type: number
identity("hello"); // returns "hello", type: string
identity(true); // returns true, type: boolean
identity({ x: 1 }); // returns { x: 1 }, type: { x: number }TypeScript infers T from the argument at each call site. You don't need to specify T explicitly — the compiler figures it out. When you call identity(42), TypeScript sees a number argument, sets T = number, and the return type becomes number.
This inference is powerful. It means generic code reads almost identically to non-generic code at the call site.
Generic Type Parameters
T is just a variable — it can appear anywhere in the function signature, multiple times, in nested positions, and in the function body type annotations. What matters is the relationship it establishes.
function wrap<T>(value: T): { value: T } {
return { value };
}
const wrapped = wrap(42);
// wrapped: { value: number }
// wrapped.value is number — TypeScript knows thisThe type parameter T ties together the input and output types. When TypeScript resolves T = number from the argument, it applies that everywhere T appears in the signature.
You can also use type parameters to constrain how a function's parameters relate to each other:
function replaceWith<T>(original: T, replacement: T): T {
return replacement;
}
replaceWith(1, 2); // OK — both number
replaceWith("a", "b"); // OK — both string
replaceWith(1, "b"); // Error — number vs stringThe single type parameter T requires both arguments to have the same type. TypeScript catches the mismatch at compile time.
Type Parameters Are Resolved Per Call
Each call to a generic function gets its own resolution of T. Calling
identity(42) resolves T = number for that call. Calling
identity("hello") independently resolves T = string for that call. There
is no shared state between calls — each is fully independent.
Multiple Type Parameters
A function can have more than one type parameter. Each gets its own letter and can vary independently:
function pair<A, B>(first: A, second: B): [A, B] {
return [first, second];
}
const p = pair(1, "hello");
// p: [number, string]
// p[0] is number, p[1] is stringHere A and B are resolved independently based on the two arguments. They can be the same type or different types — TypeScript figures it out from the call.
Another example — a function that maps a value from one type to another:
function transform<TInput, TOutput>(
value: TInput,
fn: (input: TInput) => TOutput
): TOutput {
return fn(value);
}
const length = transform("hello", (s) => s.length);
// TInput = string, TOutput = number
// length: number
const doubled = transform(5, (n) => n * 2);
// TInput = number, TOutput = number
// doubled: numberTypeScript infers both TInput from the first argument and TOutput from the return type of the callback. The descriptive names TInput and TOutput make the purpose of each type parameter clear.
Working with Generic Arrays
Arrays are one of the most common uses of generics. Many utility functions that work with arrays are naturally generic — the logic doesn't depend on the element type.
function last<T>(arr: T[]): T | undefined {
return arr[arr.length - 1];
}
function reverse<T>(arr: T[]): T[] {
return [...arr].reverse();
}
function unique<T>(arr: T[]): T[] {
return [...new Set(arr)];
}
function chunk<T>(arr: T[], size: number): T[][] {
const result: T[][] = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
}Each of these works correctly for any element type and preserves that type information through the return:
const nums = [1, 2, 3, 4, 5];
const strs = ["a", "b", "c", "a"];
last(nums); // number | undefined
reverse(strs); // string[]
unique(strs); // string[]
chunk(nums, 2); // number[][]No any. No casts. Full type safety for every element type.
TypeScript's built-in Array<T> type and the T[] shorthand are themselves generic. When you write number[], that's shorthand for Array<number>. The built-in array methods like .map(), .filter(), and .reduce() are all generic functions defined on that generic interface. Understanding generics means understanding how the standard library works.
Real-World Examples
A type-safe cache:
class Cache<T> {
private store = new Map<string, T>();
set(key: string, value: T): void {
this.store.set(key, value);
}
get(key: string): T | undefined {
return this.store.get(key);
}
has(key: string): boolean {
return this.store.has(key);
}
}
const userCache = new Cache<{ id: number; name: string }>();
userCache.set("u1", { id: 1, name: "Alice" });
const user = userCache.get("u1"); // { id: number; name: string } | undefinedThe Cache class is parameterised over T — the type of values it stores. Once you create new Cache<User>(), TypeScript knows that get() returns User | undefined and that set() requires a User value.
A result wrapper:
type Result<T> = { success: true; data: T } | { success: false; error: string };
function parseJSON<T>(json: string): Result<T> {
try {
return { success: true, data: JSON.parse(json) as T };
} catch (e) {
return { success: false, error: String(e) };
}
}
const result = parseJSON<{ name: string }>('{"name":"Alice"}');
if (result.success) {
console.log(result.data.name); // TypeScript knows: data is { name: string }
}The Result<T> type pattern — a discriminated union parameterised over the success value type — is used extensively in TypeScript codebases as a structured alternative to throwing exceptions.
An event emitter:
type EventMap = Record<string, unknown>;
class TypedEmitter<T extends EventMap> {
private handlers: Partial<{ [K in keyof T]: Array<(data: T[K]) => void> }> =
{};
on<K extends keyof T>(event: K, handler: (data: T[K]) => void): void {
if (!this.handlers[event]) this.handlers[event] = [];
this.handlers[event]!.push(handler);
}
emit<K extends keyof T>(event: K, data: T[K]): void {
this.handlers[event]?.forEach((h) => h(data));
}
}
type AppEvents = {
userLogin: { userId: string; timestamp: number };
pageView: { path: string };
};
const emitter = new TypedEmitter<AppEvents>();
emitter.on("userLogin", (data) => {
// data: { userId: string; timestamp: number }
console.log(data.userId);
});
emitter.emit("pageView", { path: "/home" });Generics Unlock the Standard Library
Once you understand generics, TypeScript's standard library becomes readable. Promise<T>, Map<K, V>, Set<T>, Array<T> — all generic. The fetch API returns Promise<Response>. When you call .json() on a response and cast it, you're manually providing the T. Libraries like React use generics extensively — useState<T>, useRef<T>, React.FC<Props>.
Best Practices
Name type parameters meaningfully for complex functions. T is fine for simple, single-purpose parameters. When a function has multiple type parameters with distinct roles, descriptive names prevent confusion:
// Harder to read — what is A and B?
function mapRecord<A, B>(
record: Record<string, A>,
fn: (v: A) => B
): Record<string, B>;
// Clearer — TValue and TMapped signal the relationship
function mapRecord<TValue, TMapped>(
record: Record<string, TValue>,
fn: (v: TValue) => TMapped
): Record<string, TMapped>;Don't use generics when a concrete type works. If a function always takes and returns string, making it generic doesn't add value — it just adds noise. Reach for generics when the function genuinely needs to work across types.
Avoid unnecessary explicit type arguments at call sites. If TypeScript can infer the type parameter, let it. Explicit type arguments like identity<number>(42) are redundant — identity(42) is clearer. Explicit type arguments are useful when inference doesn't work or when you want to document intent.
// Redundant — TypeScript infers T = number
const n = identity<number>(42);
// Necessary — TypeScript can't infer T from no arguments
const arr = new Array<string>();Avoid T extends any constraints. T extends any is equivalent to an unconstrained T but signals that the author wasn't sure what they wanted. Use unconstrained T when you mean unconstrained.
Key Takeaways
- Generics solve the code duplication problem that arises when the same logic applies to multiple types — write once, type-safe for every type
- A type parameter
<T>is a placeholder that TypeScript resolves at each call site based on the arguments provided - TypeScript infers type parameters automatically — you rarely need to specify them explicitly
- Multiple type parameters let you express relationships between the types of different parameters and return values
- Generic arrays, caches, results, and event emitters are the most common real-world generic patterns
Ready for Generic Functions
You now understand why generics exist and how type parameters work. The next post goes deeper into generic functions — specifically how TypeScript's inference engine resolves type parameters from complex function signatures, when inference falls short, and how to write generic arrow functions.