| |

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. any is 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) declares T as 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 write identity('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 extends for constraints: It’s the same keyword as inheritance but a different meaning. T extends U as a constraint means “T must be assignable to U.” It’s a requirement, not an inheritance relationship. Once constrained, T can be used as U inside 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 keyof constraint 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 keyof and indexed access are so common: Most useful generics deal with objects โ€” extracting properties, grouping by keys, building maps. keyof T and T[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 is T[K][]
  • findBy โ€” finds by key/value, value type is T[K]
  • sortBy โ€” sorts by a key
  • pick โ€” returns a subset of properties using Pick<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

FormExample
Basicfunction f<T>(x: T): T { }
Multiplefunction f<A, B>(a: A, b: B): [A, B] { }
Arrowconst f = <T>(x: T): T => x
Arrow (.tsx)const f = <T,>(x: T): T => x
Methodclass C { m<T>(x: T): T { } }
Defaultfunction f<T = string>() { }

Type Parameter Conventions

NameMeaning
TGeneral type
U, VAdditional types
KKey
VValue
EElement or error
A, BPaired types
RReturn

Inference Sources

SourceExample
Argumentf(42) โ†’ T = number
Arrayhead([1]) โ†’ T = number
Callbackf(x => ...) โ†’ T from callback
ContextReturn type of variable
Explicitf<string>(...)

Constraints

ConstraintMeaning
<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

TypeMeaning
keyof TUnion 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 caseGeneric?
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

ApproachSafeFlexibleReusable
anyโŒโœ…โœ…
Overloadsโœ…โš ๏ธโš ๏ธ
Genericsโœ…โœ…โœ…
Union typesโœ…โš ๏ธโŒ
Per-type functionsโœ…โŒโŒ

Common Generic Patterns

PatternSignature
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[]
Resulttype 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

ErrorCauseFix
T is not assignable to ...Missing constraintAdd <T extends ...>
Property 'x' does not exist on type TNo constraintConstrain to shape with x
Type argument not providedCouldn’t inferSpecify <T> explicitly
Expected 1 type argumentWrong arityMatch type parameter count
Untyped function callsNo genericAdd <T>

Arrow Function in .tsx

SyntaxWorks in .tsWorks 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

PitfallProblemSolution
Missing constraintCan’t access propertyAdd <T extends ...>
Not inferrableMust specify <T>Restructure or document
Unused type parameterWarning / no benefitRemove it
Shadowed type paramsName collisionRename
<T> in .tsxJSX conflictUse <T,>
Explicit <T> everywhereVerboseTrust inference
Over-constrainedToo restrictiveLoosen the constraint
Generic on a simple caseOverkillUse a union or specific type
Type param only on returnCan’t inferMove 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

ConceptMeaning
Generic functionFunction with type parameters
Type parameterPlaceholder โ€” <T>
InferenceTypeScript figures out T
Explicit type arg<T> at call site
ConstraintT extends U โ€” requirement
keyofUnion of keys
T[K]Indexed access
Utility typesPick, Omit, Partial, etc.

Key takeaways:

  • Generics preserve type relationships that any loses
  • Type parameters are declared with <T> after the function name
  • Inference figures out T from arguments โ€” explicit <T> is rarely needed
  • Constraints (T extends U) require T to have a specific shape
  • keyof T restricts type parameters to actual keys
  • T[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!