TypeScript — Function Types
Functions are the core building blocks of any TypeScript program. TypeScript enhances JavaScript functions with parameter type annotations, return type annotations, overloads, and this parameter typing. Understanding function types is essential for writing robust, self-documenting code.
TypeScript lets you annotate both parameter types and return types. Return types are often inferred, but explicit annotations catch bugs at the call site.
| 1 | // Function declaration with annotations |
| 2 | function add(a: number, b: number): number { |
| 3 | return a + b; |
| 4 | } |
| 5 | |
| 6 | // Arrow function |
| 7 | const multiply = (a: number, b: number): number => a * b; |
| 8 | |
| 9 | // Return type is inferred — but explicit is often better |
| 10 | function greet(name: string): string { |
| 11 | return `Hello, ${name}!`; |
| 12 | } |
| 13 | |
| 14 | // void return — function has no meaningful return value |
| 15 | function log(message: string): void { |
| 16 | console.log(message); |
| 17 | } |
| 18 | |
| 19 | // implicit return type: void |
| 20 | function noop(): void { |
| 21 | return undefined; |
| 22 | } |
| 23 | |
| 24 | // never — function never returns (throws or loops forever) |
| 25 | function throwError(msg: string): never { |
| 26 | throw new Error(msg); |
| 27 | } |
| 28 | |
| 29 | function infiniteLoop(): never { |
| 30 | while (true) {} |
| 31 | } |
TypeScript supports optional parameters (suffix ?), default parameters, and rest parameters (prefix ...). Optional parameters must come after required ones.
| 1 | // Optional parameter |
| 2 | function buildUrl(base: string, path?: string): string { |
| 3 | return path ? `${base}/${path}` : base; |
| 4 | } |
| 5 | console.log(buildUrl("https://api.com")); // https://api.com |
| 6 | console.log(buildUrl("https://api.com", "users")); // https://api.com/users |
| 7 | |
| 8 | // Default parameter |
| 9 | function createUser( |
| 10 | name: string, |
| 11 | role: string = "viewer", |
| 12 | active: boolean = true |
| 13 | ): object { |
| 14 | return { name, role, active }; |
| 15 | } |
| 16 | createUser("Alice"); // { name: "Alice", role: "viewer", active: true } |
| 17 | createUser("Bob", "admin"); // { name: "Bob", role: "admin", active: true } |
| 18 | createUser("Carol", "editor", false); |
| 19 | |
| 20 | // Rest parameter — collects arguments into an array |
| 21 | function sum(...numbers: number[]): number { |
| 22 | return numbers.reduce((total, n) => total + n, 0); |
| 23 | } |
| 24 | sum(1, 2, 3); // 6 |
| 25 | sum(10, 20, 30, 40); // 100 |
| 26 | |
| 27 | // Rest after required params |
| 28 | function logWithPrefix(prefix: string, ...messages: string[]): void { |
| 29 | messages.forEach((msg) => console.log(`[${prefix}] ${msg}`)); |
| 30 | } |
| 31 | logWithPrefix("INFO", "connected", "ready"); |
| 32 | |
| 33 | // Combined: required, optional, default, rest |
| 34 | function request( |
| 35 | url: string, |
| 36 | method: string = "GET", |
| 37 | options?: object, |
| 38 | ...middleware: Array<(req: object) => object> |
| 39 | ): object { |
| 40 | let req: object = { url, method, ...options }; |
| 41 | return middleware.reduce((acc, fn) => fn(acc), req); |
| 42 | } |
info
When you need to describe the type of a function that also has properties, use a call signature in an object type. This is common for callback-based APIs and factory functions.
| 1 | // Simple function type alias |
| 2 | type Formatter = (input: string) => string; |
| 3 | |
| 4 | // Call signature — function type with additional properties |
| 5 | interface DescribableFunction { |
| 6 | (input: string): string; |
| 7 | description: string; |
| 8 | } |
| 9 | |
| 10 | function createFormatter(desc: string): DescribableFunction { |
| 11 | const fn = (input: string) => input.toUpperCase(); |
| 12 | fn.description = desc; |
| 13 | return fn; |
| 14 | } |
| 15 | |
| 16 | const shout: DescribableFunction = createFormatter("Shout formatter"); |
| 17 | console.log(shout("hello")); // HELLO |
| 18 | console.log(shout.description); // Shout formatter |
| 19 | |
| 20 | // Constructor signature |
| 21 | interface ClockConstructor { |
| 22 | new (hour: number, minute: number): ClockInterface; |
| 23 | } |
| 24 | |
| 25 | interface ClockInterface { |
| 26 | tick(): void; |
| 27 | } |
| 28 | |
| 29 | class DigitalClock implements ClockInterface { |
| 30 | constructor(private h: number, private m: number) {} |
| 31 | tick(): void { |
| 32 | console.log(`beep beep ${this.h}:${this.m}`); |
| 33 | } |
| 34 | } |
| 35 | |
| 36 | function createClock( |
| 37 | ctor: ClockConstructor, |
| 38 | hour: number, |
| 39 | minute: number |
| 40 | ): ClockInterface { |
| 41 | return new ctor(hour, minute); |
| 42 | } |
| 43 | |
| 44 | createClock(DigitalClock, 12, 0).tick(); // beep beep 12:0 |
TypeScript uses a fake this parameter to type the this context. The first parameter named this is stripped at runtime but used for type checking.
| 1 | interface UIElement { |
| 2 | addClickListener( |
| 3 | this: UIElement, |
| 4 | handler: (this: UIElement, event: Event) => void |
| 5 | ): void; |
| 6 | } |
| 7 | |
| 8 | class Button implements UIElement { |
| 9 | private listeners: Array<(event: Event) => void> = []; |
| 10 | |
| 11 | addClickListener( |
| 12 | this: Button, |
| 13 | handler: (this: Button, event: Event) => void |
| 14 | ): void { |
| 15 | this.listeners.push((e) => handler.call(this, e)); |
| 16 | } |
| 17 | } |
| 18 | |
| 19 | // Without this parameter — loses context |
| 20 | const btn = new Button(); |
| 21 | const handler = function (this: Button, event: Event) { |
| 22 | console.log("clicked!"); |
| 23 | }; |
| 24 | |
| 25 | // Common pattern: ensure method is called on correct object |
| 26 | function assertDefined<T>(this: T, field: keyof T): void { |
| 27 | if (this[field] === undefined) { |
| 28 | throw new Error(`${String(field)} is required`); |
| 29 | } |
| 30 | } |
| 31 | |
| 32 | const obj = { name: "Alice", age: undefined as number | undefined }; |
| 33 | // assertDefined.call(obj, "name"); // OK |
| 34 | // assertDefined.call(obj, "age"); // Error at runtime: age is required |
warning
TypeScript allows multiple overload signatures for a single function implementation. Each overload is a different call pattern with its own types.
| 1 | // Overload signatures — what callers see |
| 2 | function makeDate(timestamp: number): Date; |
| 3 | function makeDate(year: number, month: number, day: number): Date; |
| 4 | |
| 5 | // Implementation signature — the actual body |
| 6 | function makeDate( |
| 7 | yearOrTimestamp: number, |
| 8 | month?: number, |
| 9 | day?: number |
| 10 | ): Date { |
| 11 | if (month !== undefined && day !== undefined) { |
| 12 | return new Date(yearOrTimestamp, month - 1, day); |
| 13 | } |
| 14 | return new Date(yearOrTimestamp); |
| 15 | } |
| 16 | |
| 17 | makeDate(1700000000000); // Date — timestamp overload |
| 18 | makeDate(2024, 1, 15); // Date — year/month/day overload |
| 19 | // makeDate(2024, 1); // Error: no matching overload |
info
| 1 | // void — function does not return a value |
| 2 | function warn(msg: string): void { |
| 3 | console.warn(msg); |
| 4 | // no return statement (or returns undefined) |
| 5 | } |
| 6 | |
| 7 | // undefined — function explicitly returns undefined |
| 8 | function returnUndefined(): undefined { |
| 9 | return undefined; |
| 10 | } |
| 11 | |
| 12 | // never — function never returns at all |
| 13 | function throwError(msg: string): never { |
| 14 | throw new Error(msg); |
| 15 | } |
| 16 | |
| 17 | function infinite(): never { |
| 18 | while (true) {} |
| 19 | } |
| 20 | |
| 21 | // Exhaustive check pattern using never |
| 22 | type Shape = |
| 23 | | { kind: "circle"; radius: number } |
| 24 | | { kind: "rectangle"; width: number; height: number }; |
| 25 | |
| 26 | function area(shape: Shape): number { |
| 27 | switch (shape.kind) { |
| 28 | case "circle": |
| 29 | return Math.PI * shape.radius ** 2; |
| 30 | case "rectangle": |
| 31 | return shape.width * shape.height; |
| 32 | default: |
| 33 | const _exhaustive: never = shape; // compile error if missing case |
| 34 | return _exhaustive; |
| 35 | } |
| 36 | } |
| 37 | |
| 38 | // void in callback position — return value is ignored |
| 39 | function forEach<T>(arr: T[], callback: (item: T) => void): void { |
| 40 | for (const item of arr) { |
| 41 | callback(item); |
| 42 | } |
| 43 | } |
| 44 | |
| 45 | // Assigning void-typed functions: |
| 46 | // A function returning void can still return a value, |
| 47 | // but callers are not allowed to use it. |
| 48 | function doSomething(): void { |
| 49 | return 42; // OK! But callers can't use the value. |
| 50 | } |
best practice
1. Always annotate return types on exported functions — they serve as documentation and catch accidental changes.
2. Use this parameters in class methods and callbacks to prevent detached-context bugs.
3. Prefer default parameters over optional parameters when a sensible fallback exists — it simplifies the call site.
4. Rest parameters are preferable to arguments — they are typed and create a proper array.
5. Use never return type for exhaustive checks in switch statements — this pattern catches missing cases at compile time.
6. Keep function overloads minimal. If you find yourself writing many overloads, consider generics or union types instead.
7. Use call signatures (interface with ()) when the function needs additional properties. Otherwise, a simple function type alias suffices.
8. Avoid void as a return type on generic callbacks — it prevents callers from using the return value even when it is meaningful.
Prefer a function type alias for plain callables. Use an interface with a call signature when the function also carries properties (e.g. jest.fn-style APIs).
| 1 | type Greeter = (name: string) => string; |
| 2 | |
| 3 | interface GreeterWithMeta { |
| 4 | (name: string): string; |
| 5 | locale: string; |
| 6 | setLocale(locale: string): void; |
| 7 | } |
| 8 | |
| 9 | const greet: GreeterWithMeta = Object.assign( |
| 10 | (name: string) => `[${greet.locale}] Hello ${name}`, |
| 11 | { |
| 12 | locale: "en", |
| 13 | setLocale(locale: string) { |
| 14 | greet.locale = locale; |
| 15 | }, |
| 16 | }, |
| 17 | ); |
Contextual typing
When a function expression appears in a typed position, TypeScript infers parameter types from context — annotate when the function is standalone or exported.
| 1 | const nums = [1, 2, 3]; |
| 2 | nums.map((n) => n * 2); // n: number from contextual typing |
| 3 | |
| 4 | export function double(n: number): number { |
| 5 | return n * 2; |
| 6 | } // annotate exports |
An async function always returns a Promise. Annotate the resolved type, not the Promise wrapper, via Promise<T>.
| 1 | async function loadUser(id: string): Promise<User> { |
| 2 | const res = await fetch(`/api/users/${id}`); |
| 3 | if (!res.ok) throw new Error("load failed"); |
| 4 | return res.json() as Promise<User>; // prefer Zod parse in real apps |
| 5 | } |
| 6 | |
| 7 | type User = { id: string; name: string }; |
| 8 | |
| 9 | async function main() { |
| 10 | try { |
| 11 | const user = await loadUser("1"); |
| 12 | console.log(user.name); |
| 13 | } catch (e) { |
| 14 | const msg = e instanceof Error ? e.message : String(e); |
| 15 | console.error(msg); |
| 16 | } |
| 17 | } |
info
Under strictFunctionTypes, parameters are checked contravariantly for function types. See the Variance guide for in/out and readonly arrays.
| 1 | type Handler<T> = (value: T) => void; |
| 2 | interface Animal { name: string } |
| 3 | interface Dog extends Animal { bark(): void } |
| 4 | |
| 5 | const ha: Handler<Animal> = (a) => console.log(a.name); |
| 6 | const hd: Handler<Dog> = ha; // OK — contravariant params |
| Position | Variance | Rule of thumb |
|---|---|---|
| Parameters | Contravariant | Wider param type is safer |
| Return type | Covariant | Narrower return is safer |
| Optional params | Compatible if assignable | Fewer required params OK |
| 1 | function first<T>(items: readonly T[]): T | undefined { |
| 2 | return items[0]; |
| 3 | } |
| 4 | |
| 5 | function pick<T, K extends keyof T>(obj: T, key: K): T[K] { |
| 6 | return obj[key]; |
| 7 | } |
| 8 | |
| 9 | const name = pick({ id: 1, name: "Ada" }, "name"); // string |
pro tip
Snippet — rest + tuple
| 1 | function pair<T, U>(a: T, b: U): [T, U] { |
| 2 | return [a, b]; |
| 3 | } |
| 4 | function merge<T extends unknown[]>(...xs: T): T { |
| 5 | return xs; |
| 6 | } |
Snippet — this param
| 1 | function click(this: HTMLButtonElement, ev: MouseEvent) { |
| 2 | console.log(this.id, ev.clientX); |
| 3 | } |
| 4 | declare const btn: HTMLButtonElement; |
| 5 | btn.addEventListener("click", click); |
note
Extra patterns for production code. Read one, type it from memory, then compare.
| 1 | export function invariant(cond: unknown, msg: string): asserts cond { |
| 2 | if (!cond) throw new Error(msg); |
| 3 | } |
| 4 | |
| 5 | export function unreachable(message = "unreachable"): never { |
| 6 | throw new Error(message); |
| 7 | } |
| 1 | export type Fn<A extends unknown[], R> = (...args: A) => R; |
| 2 | |
| 3 | export function once<A extends unknown[], R>(fn: Fn<A, R>): Fn<A, R> { |
| 4 | let called = false; |
| 5 | let result!: R; |
| 6 | return (...args: A) => { |
| 7 | if (!called) { |
| 8 | result = fn(...args); |
| 9 | called = true; |
| 10 | } |
| 11 | return result; |
| 12 | }; |
| 13 | } |
| 1 | export async function mapPool<T, R>( |
| 2 | items: readonly T[], |
| 3 | limit: number, |
| 4 | worker: (item: T, index: number) => Promise<R>, |
| 5 | ): Promise<R[]> { |
| 6 | const results: R[] = new Array(items.length); |
| 7 | let i = 0; |
| 8 | async function run() { |
| 9 | while (i < items.length) { |
| 10 | const idx = i++; |
| 11 | results[idx] = await worker(items[idx]!, idx); |
| 12 | } |
| 13 | } |
| 14 | await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => run())); |
| 15 | return results; |
| 16 | } |
info
| Helper | When to use |
|---|---|
| invariant / asserts | Narrow after runtime checks |
| unreachable / never | Exhaustiveness & dead ends |
| once | Lazy init without races in sync code |
| mapPool | Bounded async concurrency |
Community
Get help on Slack, Discord or VIP
Stuck on a guide? Join the community and ask.