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.