Functions & Generics
Typed params, generics, NoInfer, overloads.
Functions are where TypeScript earns its keep. You annotate the edges (what goes in, what comes out), and the compiler walks the rest. Once you add generics, a single function can stay strongly typed across any shape its caller throws at it.
Typing parameters and returns
// Parameter types after the colon, return type after the arrow
function add(a: number, b: number): number {
return a + b;
}
// Arrow function with explicit return type
const greet = (name: string): string => "Hi " + name;
// Often the return type can be inferred, so leave it off and let TS figure it out
const double = (n: number) => n * 2; // inferred as (n: number) => number, no annotation neededOptional, default, and rest parameters
// Optional parameter: title is string | undefined
function format(name: string, title?: string) {
return title ? title + " " + name : name;
}
// Default parameter: type is inferred from the default
function greet(name: string, greeting = "Hello") {
return greeting + ", " + name;
}
// Rest parameter: must be an array type
function sum(...nums: number[]): number {
return nums.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3); // 6
sum(...[10, 20, 30]); // 60Function types and callbacks
To describe a function as a value (e.g. a callback parameter), use the arrow syntax in a type:
// A type alias for a function
type Comparator<T> = (a: T, b: T) => number;
// Inline function type as a parameter
function map<T, U>(arr: T[], fn: (item: T, index: number) => U): U[] {
return arr.map(fn);
}
map([1, 2, 3], (n, i) => n * i); // (n, i) are inferred as numbersFunction overloads
Sometimes a function has multiple legal call signatures. Overloads let you express that without resorting to any.
// Overload signatures (just the shapes)
function parse(input: string): string;
function parse(input: number): number;
// Implementation signature (must satisfy all overloads)
function parse(input: string | number): string | number {
if (typeof input === "string") return input.trim();
return input * 2;
}
const a = parse(" hi "); // a is string
const b = parse(5); // b is number
// parse(true); // Error: no overload matchesGenerics: typed without losing information
A generic function takes a type as a parameter the same way a regular function takes a value. The result: one function that stays specific to whatever shape the caller passes.
// Without generics, the return type forgets what went in
function identityAny(x: any): any { return x; }
const r1 = identityAny("hello");
// r1 is 'any', so we just lost the fact that it is a string
// With generics, the return type tracks the input
function identity<T>(x: T): T { return x; }
const r2 = identity("hello"); // r2 is string
const r3 = identity(42); // r3 is number
const r4 = identity([1, 2]); // r4 is number[]Generic constraints
Without constraints, T could be anything (including null), so you can't use most properties on it. Use extends to require a shape.
// Require T to have a 'length' property
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest("hello", "hi"); // OK, strings have .length
longest([1, 2, 3], [9]); // OK, arrays have .length
// longest(5, 10); // Error: number has no 'length'
// Constraint with a default
function paginate<T extends object = { id: string }>(items: T[]) {
// T defaults to { id: string } if the caller does not pass it
}Inferring from arguments
You rarely have to write out the type argument. The compiler infers it from the values you pass:
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const n = first([1, 2, 3]); // T inferred as number, n is number | undefined
const s = first(["a", "b"]); // T inferred as string
const x = first<string>([]); // explicit when needed
// Multi-generic inference
function pair<A, B>(a: A, b: B): [A, B] {
return [a, b];
}
const p = pair("hi", 42); // p is [string, number]NoInfer: blocking inference where you do not want it
Sometimes you want one parameter to drive the type and another to only be checked against it, not contribute to inference. The NoInfer<T> utility (TS 5.4+) does that.
// Without NoInfer, the default value can widen T
function withDefault<T>(value: T, fallback: T): T {
return value ?? fallback;
}
// If we pass two different string literals, TS widens to a union
const r = withDefault("active" as const, "inactive");
// r is "active" | "inactive", probably not what you wanted
// With NoInfer, only 'value' contributes to inferring T
function withDefault2<T>(value: T, fallback: NoInfer<T>): T {
return value ?? fallback;
}
const r2 = withDefault2("active" as const, "active");
// r2 is exactly "active"Generic types (not just functions)
Generics work on type aliases and interfaces too. This is how you describe containers like arrays, promises, and maps.
type Box<T> = { value: T };
const numBox: Box<number> = { value: 42 };
const strBox: Box<string> = { value: "hi" };
interface ApiResponse<T> {
data: T;
error: string | null;
status: number;
}
declare function fetchUsers(): Promise<ApiResponse<User[]>>;
const users = await fetchUsers(); // users.data is User[]Try it
Below is a typed sandbox. Try changing the types passed to identity and findById, or adding a constraint to findById so the array element must have an id field.
K extends keyof Tis one of the most useful generic patterns. It says "K must be a real key of T." The compiler then knows item[key] is T[K], the exact type of that property. Auto-complete on the second argument shows only valid keys.Why is generic `identity<T>(x: T): T` better than `identity(x: any): any`?
Recap
- Annotate the edges (parameters, return types of exported functions). Let inference handle local stuff.
- Optional (
?), default (=), and rest (...) parameters all type cleanly. - Generics make a function reusable without losing type info. Use
extendsto constrain. K extends keyof Tlets you index objects safely.NoInfer<T>blocks a parameter from influencing inference.- Reach for overloads only when generics cannot express the relationship.