TypeScript 25 ๐ท Generics โ Functions
Generics let you write functions that work with many types while preserving the type relationship between input and output. Instead of function identity(x: any): any, you write function identity<T>(x: T): T โ and TypeScript understands that whatever you pass in comes back out with the same type. Generics are the mechanism behind Array<T>, Promise<T>, Map<K, V>, and virtually every library that’s both flexible and type-safe. On functions, they’re where the concept becomes practical.
Key point: A generic function takes a type parameter โ T โ that stands for whatever type the caller provides. TypeScript infers T from the arguments, so callers usually don’t write it explicitly. The generic preserves the relationship between types โ identity<string> returns string, identity<number> returns number. Without generics, you’d choose between any (no safety) and one function per type (no flexibility).
Why generics exist
Consider a function that returns its argument.
Without generics โ any:
function identity(x: any): any {
return x;
}
const a = identity('hello'); // a is any
const b = identity(42); // b is any
any accepts anything but loses type information. a and b are both any โ the compiler knows nothing about them.
Without generics โ per-type functions:
function identityString(x: string): string { return x; }
function identityNumber(x: number): number { return x; }
function identityBoolean(x: boolean): boolean { return x; }
// ... and so on for every type
Safe but tedious. Every new type needs a new function.
With generics:
function identity<T>(x: T): T {
return x;
}
const a = identity('hello'); // a is string
const b = identity(42); // b is number
const c = identity(true); // c is boolean
One function, full type safety, no duplication. T is a type parameter โ a placeholder that becomes a specific type when the function is called.
How inference works: When you call identity('hello'), TypeScript infers T = string from the argument. The return type is string. No explicit annotation needed.
Explicit type arguments:
const a = identity<string>('hello'); // T = string, explicitly
Usually unnecessary โ inference handles it. But sometimes you need to specify T when inference can’t figure it out.
Why generics matter: They’re the difference between flexible and safe.
anyis flexible but unsafe; per-type functions are safe but inflexible. Generics give you both โ one function, full type information, no duplication. That’s why every typed language has them and why they’re everywhere in TypeScript’s standard library.
Basic generic functions
A generic function declares type parameters in angle brackets after the function name.
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const n = first([1, 2, 3]); // number | undefined
const s = first(['a', 'b']); // string | undefined
T is inferred from the array’s element type. The return type is T | undefined โ a value of the same type, or nothing.
Multiple type parameters:
function pair<A, B>(a: A, b: B): [A, B] {
return [a, b];
}
const p1 = pair(1, 'a'); // [number, string]
const p2 = pair(true, { x: 1 }); // [boolean, { x: number }]
Each type parameter captures a different type.
Arrow function generics:
const identity = <T>(x: T): T => x;
const first = <T,>(arr: T[]): T | undefined => arr[0];
Note the trailing comma in <T,> for arrow functions in .tsx files โ without it, <T> is parsed as JSX. In .ts files, <T> alone is fine.
Method generics:
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}
const s = new Stack<number>();
s.push(1);
s.pop(); // number | undefined
The class’s type parameter T flows into its methods.
Why type parameter names are conventions: T for a general type, U, V for additional types, K for keys, V for values, E for elements. These are conventions, not rules โ any identifier works โ but they make code easier to read.
Why type parameters before the parentheses:
function f<T>(x: T)declaresTas a parameter of the function’s type, not of a single argument. The<T>comes before the parameter list so it’s visible before the arguments that use it. It’s the same idea as generic types in other languages โ the type parameter is part of the function’s signature.
Type inference in generics
TypeScript infers type arguments from the arguments you pass. This is why you rarely write <T> explicitly.
From arguments:
function wrap<T>(value: T): { value: T } {
return { value };
}
wrap('hello'); // T inferred as string
wrap(42); // T inferred as number
From arrays:
function head<T>(arr: T[]): T | undefined {
return arr[0];
}
head([1, 2, 3]); // T inferred as number
head(['a']); // T inferred as string
head([]); // T inferred as never (empty array)
An empty array gives T = never โ the empty type. Use head<number>([]) to specify.
From multiple arguments:
function merge<A, B>(a: A, b: B): A & B {
return { ...a, ...b } as A & B;
}
merge({ name: 'Alice' }, { age: 30 });
// inferred as { name: string } & { age: number }
A and B are inferred from the two arguments independently.
From return type context:
const fn: <T>(x: T) => T = x => x; // T comes from the declared type
When the function has an explicit type, the type parameter is bound by it.
Default type parameters:
function create<T = string>(): T[] {
return [] as T[];
}
const a = create(); // string[]
const b = create<number>(); // number[]
Defaults apply when the type can’t be inferred and isn’t specified.
When inference fails: Sometimes TypeScript can’t infer T. That’s when you write it explicitly.
function fromJson<T>(json: string): T {
return JSON.parse(json) as T;
}
const user = fromJson<User>('{"name":"Alice"}'); // must specify T
The function can’t know what shape the JSON is โ the caller has to say.
Why inference matters: It’s the ergonomics of generics. If every call required
<T>, generics would be painful. Inference means you writeidentity('hello')and get full typing without ceremony. Explicit type arguments are the escape hatch for when inference can’t work โ but they’re the exception, not the rule.
Generic constraints
Sometimes you need T to have specific properties. A constraint says “T must extend this shape.”
function getLength<T extends { length: number }>(x: T): number {
return x.length;
}
getLength('hello'); // โ
string has length
getLength([1, 2, 3]); // โ
array has length
getLength({ length: 5 }); // โ
object with length
getLength(42); // โ number has no length
The extends { length: number } constraint requires T to have a length property. Inside the function, you can access x.length because the constraint guarantees it.
Constraints with interfaces:
interface HasId {
id: string;
}
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
const users = [{ id: '1', name: 'Alice' }];
findById(users, '1'); // โ
findById([{ name: 'x' }], '1'); // โ no id property
T extends HasId ensures T has an id property. The function can use item.id.
Constraints with keyof:
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: 'Alice' };
get(user, 'name'); // string
get(user, 'id'); // number
get(user, 'missing'); // โ not a key of user
K extends keyof T restricts K to actual keys of T. The return type T[K] is the type of that property.
Multiple constraints:
function merge<T extends object, U extends object>(a: T, b: U): T & U {
return { ...a, ...b };
}
Both T and U must be objects.
Constraints with unions:
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const key: 'id' | 'name' = 'name';
get({ id: 1, name: 'x' }, key); // โ
K is constrained to the union of T‘s keys.
Why constraints: They let you access properties on T that TypeScript would otherwise reject. Without a constraint, x.length inside the function fails โ TypeScript doesn’t know T has a length. The constraint tells it.
Why
extendsfor constraints: It’s the same keyword as inheritance but a different meaning.T extends Uas a constraint means “T must be assignable to U.” It’s a requirement, not an inheritance relationship. Once constrained,Tcan be used asUinside the function.
Generic functions in practice
Where generics shine in real code.
Array utilities:
function groupBy<T, K extends string | number>(
items: T[],
key: (item: T) => K
): Record<K, T[]> {
return items.reduce((acc, item) => {
const k = key(item);
(acc[k] ??= []).push(item);
return acc;
}, {} as Record<K, T[]>);
}
const users = [
{ name: 'Alice', role: 'admin' },
{ name: 'Bob', role: 'user' },
{ name: 'Carol', role: 'admin' }
];
const byRole = groupBy(users, u => u.role);
// Record<'admin' | 'user', User[]>
Wrapping values:
function ok<T>(value: T): { ok: true; value: T } {
return { ok: true, value };
}
function err<E>(error: E): { ok: false; error: E } {
return { ok: false, error };
}
const a = ok(42); // { ok: true; value: number }
const b = err('failed'); // { ok: false; error: string }
Event emitter:
class EventEmitter<Events extends Record<string, unknown[]>> {
private handlers = new Map<keyof Events, Function[]>();
on<K extends keyof Events>(
event: K,
handler: (...args: Events[K]) => void
): void {
const list = this.handlers.get(event) ?? [];
list.push(handler);
this.handlers.set(event, list);
}
emit<K extends keyof Events>(event: K, ...args: Events[K]): void {
this.handlers.get(event)?.forEach(h => h(...args));
}
}
type AppEvents = {
click: [x: number, y: number];
keydown: [key: string];
};
const emitter = new EventEmitter<AppEvents>();
emitter.on('click', (x, y) => console.log(x, y));
emitter.on('keydown', key => console.log(key));
emitter.emit('click', 10, 20);
emitter.emit('keydown', 'Enter');
Type-safe property access:
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map(item => item[key]);
}
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
];
const names = pluck(users, 'name'); // string[]
const ids = pluck(users, 'id'); // number[]
Constraints are everywhere in real generic code: They enforce that the caller passes something the function can actually work with. Most useful generic functions have constraints.
Why generics + constraints are a pair: Generics alone say “any type.” Constraints narrow it to “any type with this shape.” Together they let you write functions that work broadly but still safely. The
keyofconstraint is one of the most common โ it enforces that a property name is actually a key of the object.
Generic functions vs overloads
When a function’s return type depends on arguments, you can use either generics or overloads. Generics are usually better.
Overloads:
function wrap(x: string): { s: string };
function wrap(x: number): { n: number };
function wrap(x: string | number): { s: string } | { n: number } {
return typeof x === 'string' ? { s: x } : { n: x };
}
Each overload is a separate signature. Verbose, and adding types means adding overloads.
Generics:
function wrap<T>(x: T): { value: T } {
return { value: x };
}
const a = wrap('hello'); // { value: string }
const b = wrap(42); // { value: number }
One signature, all types. Cleaner and extensible.
When overloads win:
function parse(x: string): object;
function parse(x: number): number;
function parse(x: string | number): object | number {
return typeof x === 'string' ? JSON.parse(x) : x;
}
Here the return types are unrelated โ string returns object, number returns number. A generic would return the same type as the input, which isn’t what we want. Overloads express the specific input-output relationship.
Rule of thumb: Use generics when the return type depends on the input’s type. Use overloads when different inputs produce genuinely different shapes.
Combining both:
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
Generic with constraint โ the best of both. The type relationship is precise, and there’s only one signature.
Why generics usually beat overloads: Overloads duplicate the signature for each type. Generics capture the pattern in one signature. If you add a new type, overloads need a new line; generics just work. Use overloads only when the input-output relationship can’t be expressed generically.
Common generic patterns
A few reusable patterns that show up everywhere.
Identity โ the simplest generic:
function identity<T>(x: T): T {
return x;
}
Rarely used directly, but it’s the foundation.
Pluck โ extract a property:
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map(item => item[key]);
}
GroupBy โ bucket items:
function groupBy<T, K extends string>(
items: T[],
keyFn: (item: T) => K
): Record<K, T[]> {
return items.reduce((acc, item) => {
const k = keyFn(item);
(acc[k] ??= []).push(item);
return acc;
}, {} as Record<K, T[]>);
}
Map array โ transform elements:
function mapArray<T, U>(items: T[], fn: (item: T) => U): U[] {
return items.map(fn);
}
Result type:
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
function ok<T>(value: T): Result<T> {
return { ok: true, value };
}
function err<E>(error: E): Result<never, E> {
return { ok: false, error };
}
Identity-based cache:
function memoize<T extends (...args: any[]) => any>(fn: T): T {
const cache = new Map<string, ReturnType<T>>();
return ((...args: Parameters<T>) => {
const key = JSON.stringify(args);
if (!cache.has(key)) cache.set(key, fn(...args));
return cache.get(key)!;
}) as T;
}
Parameters<T> and ReturnType<T> extract the parameter and return types from a function type.
Why these patterns recur: They’re the fundamental operations on collections and values โ extract, group, transform, memoize. Each one preserves type relationships that any would lose. Learning them is learning to think generically.
Why
keyofand indexed access are so common: Most useful generics deal with objects โ extracting properties, grouping by keys, building maps.keyof TandT[K]express those relationships precisely. Once you understand them, a whole class of generic utilities becomes writable.
A full example
A small type-safe query builder using generic functions.
// ============================================
// DATA
// ============================================
interface User {
id: number;
name: string;
email: string;
age: number;
}
const users: User[] = [
{ id: 1, name: 'Alice', email: 'alice@example.com', age: 30 },
{ id: 2, name: 'Bob', email: 'bob@example.com', age: 25 },
{ id: 3, name: 'Carol', email: 'carol@example.com', age: 35 }
];
// ============================================
// GENERIC UTILITIES
// ============================================
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map(item => item[key]);
}
function findBy<T, K extends keyof T>(
items: T[],
key: K,
value: T[K]
): T | undefined {
return items.find(item => item[key] === value);
}
function sortBy<T, K extends keyof T>(
items: T[],
key: K
): T[] {
return [...items].sort((a, b) => {
const av = a[key];
const bv = b[key];
if (av < bv) return -1;
if (av > bv) return 1;
return 0;
});
}
function pick<T, K extends keyof T>(item: T, keys: K[]): Pick<T, K> {
const result = {} as Pick<T, K>;
for (const key of keys) {
result[key] = item[key];
}
return result;
}
// ============================================
// USAGE
// ============================================
const names = pluck(users, 'name');
// string[]
const ages = pluck(users, 'age');
// number[]
const alice = findBy(users, 'name', 'Alice');
// User | undefined
const sorted = sortBy(users, 'age');
// User[]
const summary = pick(users[0], ['id', 'name']);
// { id: number; name: string }
console.log(names); // ['Alice', 'Bob', 'Carol']
console.log(ages); // [30, 25, 35]
console.log(alice?.email);
console.log(sorted.map(u => u.name)); // ['Bob', 'Alice', 'Carol']
console.log(summary); // { id: 1, name: 'Alice' }
What this shows:
pluckโ extracts one property from each item, return type isT[K][]findByโ finds by key/value, value type isT[K]sortByโ sorts by a keypickโ returns a subset of properties usingPick<T, K>
Every function is generic, and every return type is precise. Call pluck(users, 'name') and TypeScript knows the result is string[].
Why this shape: It’s a mini query builder. The generic utilities preserve type information through every operation. Change the data type, and the utilities still work โ no code changes. That’s the point of generics.
Complete Example Session
# ============================================
# PART 1: BASIC GENERIC
# ============================================
cat > basic.ts << 'EOF'
function identity<T>(x: T): T {
return x;
}
const a = identity('hello'); // string
const b = identity(42); // number
const c = identity(true); // boolean
console.log(a, b, c);
// Explicit type argument
const d = identity<string>('world');
console.log(d);
EOF
npx tsc --noEmit basic.ts
# (no errors)
# ============================================
# PART 2: MULTIPLE PARAMETERS
# ============================================
cat > multi.ts << 'EOF'
function pair<A, B>(a: A, b: B): [A, B] {
return [a, b];
}
const p = pair(1, 'a');
// [number, string]
function zip<A, B>(as: A[], bs: B[]): [A, B][] {
return as.map((a, i) => [a, bs[i]]);
}
console.log(p, zip([1, 2], ['a', 'b']));
EOF
npx tsc --noEmit multi.ts
# (no errors)
# ============================================
# PART 3: CONSTRAINTS
# ============================================
cat > constraints.ts << 'EOF'
function getLength<T extends { length: number }>(x: T): number {
return x.length;
}
console.log(getLength('hello'));
console.log(getLength([1, 2, 3]));
console.log(getLength({ length: 42 }));
// getLength(42); // โ number has no length
EOF
npx tsc --noEmit constraints.ts
# (no errors)
# ============================================
# PART 4: KEYOF CONSTRAINT
# ============================================
cat > keyof.ts << 'EOF'
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: 'Alice', active: true };
const n = get(user, 'name'); // string
const i = get(user, 'id'); // number
const a = get(user, 'active'); // boolean
console.log(n, i, a);
// get(user, 'missing'); // โ
EOF
npx tsc --noEmit keyof.ts
# (no errors)
# ============================================
# PART 5: PLUCK AND GROUP BY
# ============================================
cat > utils.ts << 'EOF'
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map(item => item[key]);
}
function groupBy<T, K extends string>(
items: T[],
key: (item: T) => K
): Record<K, T[]> {
return items.reduce((acc, item) => {
const k = key(item);
(acc[k] ??= []).push(item);
return acc;
}, {} as Record<K, T[]>);
}
const users = [
{ id: 1, name: 'Alice', role: 'admin' },
{ id: 2, name: 'Bob', role: 'user' },
{ id: 3, name: 'Carol', role: 'admin' }
];
console.log(pluck(users, 'name'));
console.log(groupBy(users, u => u.role));
EOF
npx tsc --noEmit utils.ts
# (no errors)
# ============================================
# PART 6: RESULT TYPE
# ============================================
cat > result.ts << 'EOF'
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
function ok<T>(value: T): Result<T> {
return { ok: true, value };
}
function err<E>(error: E): Result<never, E> {
return { ok: false, error };
}
function divide(a: number, b: number): Result<number> {
if (b === 0) return err(new Error('Division by zero'));
return ok(a / b);
}
console.log(divide(10, 2));
console.log(divide(10, 0));
EOF
npx tsc --noEmit result.ts
# (no errors)
# ============================================
# PART 7: COMPILE AND RUN
# ============================================
npx tsc basic.ts multi.ts constraints.ts keyof.ts utils.ts result.ts
node basic.js
# [ hello 42 true ]
# [ world ]
node multi.js
# [ [ 1, 'a' ] [ [ 1, 'a' ], [ 2, 'b' ] ] ]
node constraints.js
# [ 5 ]
# [ 3 ]
# [ 42 ]
node keyof.js
# [ Alice 1 true ]
node utils.js
# [ [ 'Alice', 'Bob', 'Carol' ] ]
# [ { admin: [...], user: [...] } ]
node result.js
# [ { ok: true, value: 5 } ]
# [ { ok: false, error: Error: Division by zero } ]
Quick Reference
Generic Function Syntax
| Form | Example |
|---|---|
| Basic | function f<T>(x: T): T { } |
| Multiple | function f<A, B>(a: A, b: B): [A, B] { } |
| Arrow | const f = <T>(x: T): T => x |
| Arrow (.tsx) | const f = <T,>(x: T): T => x |
| Method | class C { m<T>(x: T): T { } } |
| Default | function f<T = string>() { } |
Type Parameter Conventions
| Name | Meaning |
|---|---|
T | General type |
U, V | Additional types |
K | Key |
V | Value |
E | Element or error |
A, B | Paired types |
R | Return |
Inference Sources
| Source | Example |
|---|---|
| Argument | f(42) โ T = number |
| Array | head([1]) โ T = number |
| Callback | f(x => ...) โ T from callback |
| Context | Return type of variable |
| Explicit | f<string>(...) |
Constraints
| Constraint | Meaning |
|---|---|
<T extends U> | T must be assignable to U |
<T extends object> | T is an object type |
<K extends keyof T> | K is a key of T |
<T extends string | number> | T is a union member |
<T extends (...args: any[]) => any> | T is a function |
Common Utility Types in Generics
| Type | Meaning |
|---|---|
keyof T | Union of T’s keys |
T[K] | Type of T’s property K |
Pick<T, K> | Subset of T’s properties |
Omit<T, K> | T without K |
Partial<T> | All properties optional |
Required<T> | All properties required |
ReturnType<T> | Return type of function T |
Parameters<T> | Parameters of function T |
InstanceType<T> | Instance type of constructor T |
When to Use Generics
| Use case | Generic? |
|---|---|
| Return type depends on input | โ |
| Works with many types | โ |
| Preserves type relationships | โ |
| Array / collection utilities | โ |
| Wrappers (Result, Option) | โ |
| Type-safe property access | โ |
| Fixed types | โ |
| Simple unions | โ (usually) |
Generics vs Alternatives
| Approach | Safe | Flexible | Reusable |
|---|---|---|---|
any | โ | โ | โ |
| Overloads | โ | โ ๏ธ | โ ๏ธ |
| Generics | โ | โ | โ |
| Union types | โ | โ ๏ธ | โ |
| Per-type functions | โ | โ | โ |
Common Generic Patterns
| Pattern | Signature |
|---|---|
| Identity | <T>(x: T): T |
| Pluck | <T, K extends keyof T>(xs: T[], k: K): T[K][] |
| Group by | <T, K extends string>(xs: T[], fn): Record<K, T[]> |
| Map | <T, U>(xs: T[], fn: (x: T) => U): U[] |
| Result | type Result<T, E = Error> |
| Memoize | <T extends Function>(fn: T): T |
| Pair | <A, B>(a: A, b: B): [A, B] |
| Zip | <A, B>(as: A[], bs: B[]): [A, B][] |
Errors and Fixes
| Error | Cause | Fix |
|---|---|---|
T is not assignable to ... | Missing constraint | Add <T extends ...> |
Property 'x' does not exist on type T | No constraint | Constrain to shape with x |
Type argument not provided | Couldn’t infer | Specify <T> explicitly |
Expected 1 type argument | Wrong arity | Match type parameter count |
Untyped function calls | No generic | Add <T> |
Arrow Function in .tsx
| Syntax | Works in .ts | Works in .tsx |
|---|---|---|
<T>(x: T) => x | โ | โ (JSX conflict) |
<T,>(x: T) => x | โ | โ |
<T extends unknown>(x: T) => x | โ | โ |
Best Practices
โ Do This:
// Use generics when return type depends on input
function first<T>(arr: T[]): T | undefined {
return arr[0];
} // โ
// Constrain generics when you need properties
function getLength<T extends { length: number }>(x: T): number {
return x.length;
} // โ
// Use keyof for type-safe property access
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
} // โ
// Let inference do the work
identity('hello'); // no explicit <string> // โ
// Use descriptive parameter names
function map<T, U>(xs: T[], fn: (x: T) => U): U[] { } // โ
// Use utility types
function pick<T, K extends keyof T>(o: T, k: K[]): Pick<T, K> { } // โ
// Provide defaults when useful
function create<T = string>(): T[] { return [] as T[]; } // โ
// Use `<T,>` in .tsx arrow functions
const f = <T,>(x: T): T => x; // โ
โ Don’t Do This:
// Don't use `any` when a generic fits
function first(arr: any[]): any { return arr[0]; } // โ ๏ธ
// Don't add generics where a specific type works
function greet<T>(name: T): string { return `Hi, ${name}`; } // โ ๏ธ
// Don't forget constraints when accessing properties
function getLength<T>(x: T): number {
return x.length; // โ property does not exist // โ
}
// Don't use type parameters the input can't infer
function fromJson<T>(json: string): T { return JSON.parse(json); }
// caller must specify <T>; document this // โ ๏ธ
// Don't shadow type parameters
function f<T, T>() { } // โ duplicate // โ
// Don't use `<T>` alone in .tsx arrow functions
const f = <T>(x: T) => x; // โ parsed as JSX // โ
// Don't over-constrain
function add<T extends number>(a: T, b: T): T {
return a + b; // โ ๏ธ actually fine with number // โ ๏ธ
}
// Don't use generics for simple unions
function f<T extends string | number>(x: T): T { return x; }
// simpler: function f(x: string | number) { } // โ ๏ธ
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
| Missing constraint | Can’t access property | Add <T extends ...> |
| Not inferrable | Must specify <T> | Restructure or document |
| Unused type parameter | Warning / no benefit | Remove it |
| Shadowed type params | Name collision | Rename |
<T> in .tsx | JSX conflict | Use <T,> |
Explicit <T> everywhere | Verbose | Trust inference |
| Over-constrained | Too restrictive | Loosen the constraint |
| Generic on a simple case | Overkill | Use a union or specific type |
| Type param only on return | Can’t infer | Move to parameters |
Real-World Examples
1. Identity
function identity<T>(x: T): T { return x; }
2. First element
function first<T>(arr: T[]): T | undefined { return arr[0]; }
3. Pair
function pair<A, B>(a: A, b: B): [A, B] { return [a, b]; }
4. Get with keyof
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
5. Pluck
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map(i => i[key]);
}
6. Group by
function groupBy<T, K extends string>(
items: T[],
key: (t: T) => K
): Record<K, T[]> {
return items.reduce((acc, item) => {
const k = key(item);
(acc[k] ??= []).push(item);
return acc;
}, {} as Record<K, T[]>);
}
7. Map
function map<T, U>(xs: T[], fn: (x: T) => U): U[] {
return xs.map(fn);
}
8. Find by
function findBy<T, K extends keyof T>(
items: T[], key: K, value: T[K]
): T | undefined {
return items.find(i => i[key] === value);
}
9. Filter by
function filterBy<T, K extends keyof T>(
items: T[], key: K, value: T[K]
): T[] {
return items.filter(i => i[key] === value);
}
10. Sort by
function sortBy<T, K extends keyof T>(items: T[], key: K): T[] {
return [...items].sort((a, b) => a[key] < b[key] ? -1 : 1);
}
11. Pick
function pick<T, K extends keyof T>(item: T, keys: K[]): Pick<T, K> {
return keys.reduce((acc, k) => {
acc[k] = item[k];
return acc;
}, {} as Pick<T, K>);
}
12. Result type
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
13. Ok constructor
function ok<T>(value: T): Result<T> {
return { ok: true, value };
}
14. Memoize
function memoize<T extends (...args: any[]) => any>(fn: T): T {
const cache = new Map<string, ReturnType<T>>();
return ((...args: Parameters<T>) => {
const key = JSON.stringify(args);
if (!cache.has(key)) cache.set(key, fn(...args));
return cache.get(key)!;
}) as T;
}
15. Wrap
function wrap<T>(value: T): { value: T } {
return { value };
}
16. Constrain to length
function getLength<T extends { length: number }>(x: T): number {
return x.length;
}
17. Default type param
function create<T = string>(): T[] { return [] as T[]; }
18. Arrow function with constraint
const get = <T, K extends keyof T>(o: T, k: K): T[K] => o[k];
19. Generic class method
class Box<T> {
constructor(public value: T) {}
map<U>(fn: (v: T) => U): Box<U> {
return new Box(fn(this.value));
}
}
20. Generic with multiple constraints
function merge<T extends object, U extends object>(a: T, b: U): T & U {
return { ...a, ...b };
}
Visual: Generic Function Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ function identity<T>(x: T): T { return x; } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ called with
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ identity('hello') โ
โ โ โ
โ โผ โ
โ T inferred as string โ
โ โ โ
โ โผ โ
โ return type is string โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ identity(42) โ
โ โ โ
โ โผ โ
โ T inferred as number โ
โ โ โ
โ โผ โ
โ return type is number โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Without vs With Generics
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Without generics โ any โ
โ โ
โ function identity(x: any): any { return x; }โ
โ โ
โ const a = identity('hello'); โ
โ a is any โ no type information โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ With generics โ
โ โ
โ function identity<T>(x: T): T { return x; } โ
โ โ
โ const a = identity('hello'); โ
โ a is string โ full type information โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Constraints
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Without constraint โ
โ โ
โ function f<T>(x: T) { โ
โ return x.length; โ โ
โ } โ
โ โ
โ T could be anything โ no length property โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ With constraint โ
โ โ
โ function f<T extends { length: number }>(x: T)โ
โ return x.length; โ
โ
โ } โ
โ โ
โ T guaranteed to have length โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: keyof Constraint
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ function get<T, K extends keyof T>( โ
โ obj: T, key: K โ
โ ): T[K] { โ
โ return obj[key]; โ
โ } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ const user = { id: 1, name: 'Alice' }; โ
โ โ
โ get(user, 'name') โ string โ
โ get(user, 'id') โ number โ
โ get(user, 'x') โ โ not a key โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Inference
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Call: identity('hello') โ
โ โ
โ Compiler: โ
โ 1. Sees argument 'hello' โ
โ 2. Infers T = string โ
โ 3. Return type becomes string โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Call: pair(1, 'a') โ
โ โ
โ Compiler: โ
โ 1. First arg โ A = number โ
โ 2. Second arg โ B = string โ
โ 3. Return type becomes [number, string] โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Common Utility Types
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ T โ the type itself โ
โ keyof T โ union of property names โ
โ T[K] โ type of property K โ
โ Pick<T, K> โ subset with keys K โ
โ Omit<T, K> โ T without keys K โ
โ Partial<T> โ all properties optional โ
โ Required<T> โ all properties required โ
โ ReturnType<T> โ return type of function โ
โ Parameters<T> โ parameters of function โ
โ InstanceType<T> โ instance type of class โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Generics vs Overloads
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Overloads โ
โ โ
โ function f(x: string): string; โ
โ function f(x: number): number; โ
โ function f(x: boolean): boolean; โ
โ function f(x: any): any { return x; } โ
โ โ
โ Each type needs a line โ
โ Adding a type = adding a line โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Generic โ
โ โ
โ function f<T>(x: T): T { return x; } โ
โ โ
โ One signature, all types โ
โ New types work automatically โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: <T,> in .tsx
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ In .ts file โ
โ โ
โ const f = <T>(x: T) => x; โ
โ
โ โ
โ Parsed as generic function โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ In .tsx file โ
โ โ
โ const f = <T>(x: T) => x; โ โ
โ // Parsed as JSX element โ
โ โ
โ const f = <T,>(x: T) => x; โ
โ
โ // Trailing comma forces generic parse โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Decision Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Does the return type depend on the input? โ
โ โ โ
โ โโโ Yes โโโบ Generic โ
โ โ โ
โ โโโ No โโโบ Specific type or union โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Do you need a property on T? โ
โ โ โ
โ โโโ Yes โโโบ Add a constraint โ
โ โ โ
โ โโโ No โโโบ Plain generic โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Is the relationship expressible generically?โ
โ โ โ
โ โโโ Yes โโโบ Generic โ
โ โ โ
โ โโโ No โโโบ Overloads โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Concept | Meaning |
|---|---|
| Generic function | Function with type parameters |
| Type parameter | Placeholder โ <T> |
| Inference | TypeScript figures out T |
| Explicit type arg | <T> at call site |
| Constraint | T extends U โ requirement |
keyof | Union of keys |
T[K] | Indexed access |
| Utility types | Pick, Omit, Partial, etc. |
Key takeaways:
- Generics preserve type relationships that
anyloses - Type parameters are declared with
<T>after the function name - Inference figures out
Tfrom arguments โ explicit<T>is rarely needed - Constraints (
T extends U) requireTto have a specific shape keyof Trestricts type parameters to actual keysT[K]gives the type of a property- Multiple type parameters โ
<A, B>โ capture multiple types - Utility types โ
Pick,Omit,Partial,ReturnType,Parametersโ work with generics - Use generics when the return type depends on the input’s type
- Use overloads when the input-output relationship can’t be expressed generically
- In
.tsx, use<T,>for arrow generics to avoid JSX parsing - Common patterns โ
identity,pluck,groupBy,map,Result<T>,memoize - Don’t overuse generics โ if a specific type or union works, prefer it
Remember: Generics are what make a function both flexible and safe. They let one function work with many types while preserving the exact type relationships the caller cares about. Declare <T>, constrain it when you need properties, and let inference do the rest. The result is code that adapts to what it’s given, catches mistakes, and reads cleanly โ no any, no per-type duplication. That’s the whole point of generics.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!