Polyglot
Languages TypeScript functions
TypeScript § functions

Functions

TypeScript inherits JavaScript’s function surface and adds type annotations. The principal forms are function declarations (function name() {} — hoisted, statement form), function expressions (const f = function() {}), and arrow functions (const f = () => {} — concise, with lexical this). Each form admits parameter and return type annotations. TypeScript adds function overloading (multiple call signatures), this parameters (typing the receiver), generic functions, and the never return type for non-returning functions. The combination — typed JavaScript functions, overloading, generics, the this parameter — covers the routine function surface.

Function declarations

The principal form:

function add(a: number, b: number): number {
    return a + b;
}

function greet(name: string): void {
    console.log(`Hello, ${name}`);
}

Function declarations are hoisted to the top of their scope:

hello();                                          // OK; function is hoisted
function hello() { console.log("hi"); }

The form admits explicit parameter types and an explicit return type. The compiler infers the return type from the body if omitted; the conventional discipline annotates return types on exported functions.

Function expressions

const add = function (a: number, b: number): number {
    return a + b;
};

const named = function namedExpr(x: number): number {
    return x;                                     // namedExpr is in scope inside the function
};

Function expressions are not hoisted — the binding (add) is hoisted with let/const/var rules, but the function value is only available after the assignment.

Arrow functions

The concise form:

const add = (a: number, b: number): number => a + b;

const greet = (name: string): void => {
    console.log(`Hello, ${name}`);
};

const square = (n: number) => n * n;             // return type inferred
const log = (msg: string) => { console.log(msg); };  // body block

// Single parameter, no parens needed:
const double = n => n * 2;                       // (any-typed; conventional contemporary requires types)

Arrow functions have lexical this — they do not introduce their own this binding:

class Counter {
    count = 0;

    // Arrow: this is the Counter instance:
    increment = () => {
        this.count++;
    };

    // Method: this depends on call site:
    decrement() {
        this.count--;
    }
}

const c = new Counter();
const inc = c.increment;                          // OK; `this` is bound
inc();                                            // count = 1
const dec = c.decrement;
// dec();                                          // ERROR: this is undefined

The lexical this is one of the principal differences from function-declared methods.

Optional parameters

A trailing ? marks a parameter as optional:

function greet(name: string, greeting?: string): string {
    return `${greeting ?? "Hello"}, ${name}`;
}

greet("Alice");                                   // "Hello, Alice"
greet("Alice", "Hi");                             // "Hi, Alice"

Optional parameters must come after required parameters; the parameter type is implicitly T | undefined.

Default parameters

Default values are admitted:

function greet(name = "world", greeting = "Hello"): string {
    return `${greeting}, ${name}`;
}

greet();                                          // "Hello, world"
greet("Alice");                                   // "Hello, Alice"
greet("Alice", "Hi");                             // "Hi, Alice"
greet(undefined, "Hi");                           // "Hi, world" (use default)

The compiler infers the parameter type from the default value:

function add(a: number, b = 0): number {         // b: number = 0
    return a + b;
}

Defaults make a parameter implicitly optional.

Rest parameters

A rest parameter (the last parameter, prefixed with ...) collects extra arguments:

function sum(...nums: number[]): number {
    return nums.reduce((a, b) => a + b, 0);
}

sum();                                            // 0
sum(1, 2, 3);                                     // 6

// Spreading into a rest parameter:
const xs = [1, 2, 3];
sum(...xs);                                       // 6

The rest parameter must be the last parameter; its type must be an array or tuple type.

For substantial typing, rest in tuple types admits substantial precision:

type Args = [string, ...number[]];
function process(...args: Args) { /* args[0] is string; rest are number */ }

Function types

Three principal forms:

// Function type expression:
type Adder = (a: number, b: number) => number;

const add: Adder = (a, b) => a + b;

// Call signature in object/interface:
interface Comparator<T> {
    (a: T, b: T): number;
}

const compareNumbers: Comparator<number> = (a, b) => a - b;

// Construct signature (for constructors):
interface UserConstructor {
    new (name: string, age: number): User;
}

The arrow form ((a: number, b: number) => number) is the conventional concise form for function types.

Function overloading

A function may have multiple overload signatures with one implementation signature:

function pad(value: string, length: number): string;
function pad(value: number, length: number): number;
function pad(value: string | number, length: number): string | number {
    if (typeof value === "string") {
        return value.padStart(length, "0");
    }
    return Number(String(value).padStart(length, "0"));
}

const a = pad("42", 5);                          // "00042" (string)
const b = pad(42, 5);                            // 42 (number)

The overload signatures appear above; the implementation signature must be compatible with all overloads. Callers see only the overload signatures.

The conventional discipline:

  • Use overloads when the return type depends on the argument types.
  • Prefer union types and conditional return types when overloads do not add clarity.
  • Keep the implementation signature broad enough to accept all overloads.

The this parameter

A first parameter named this admits typing the receiver:

interface User {
    name: string;
    greet(this: User): string;
}

const greet = function (this: User): string {
    return `Hello, ${this.name}`;
};

greet.call({ name: "Alice" });                   // OK
// const u = { name: "Alice" }; greet();          // ERROR: this is wrong type

The this parameter is erased at runtime — it is purely a type-system feature. The conventional uses are typing methods that may be detached and called on different receivers.

Generic functions

Type parameters appear in angle brackets:

function identity<T>(x: T): T {
    return x;
}

function pair<A, B>(a: A, b: B): [A, B] {
    return [a, b];
}

function map<T, U>(arr: readonly T[], fn: (x: T) => U): U[] {
    return arr.map(fn);
}

Treated in Generics.

Higher-order functions

Functions that take or return functions:

function applyTwice(fn: (x: number) => number, x: number): number {
    return fn(fn(x));
}

const result = applyTwice(n => n + 1, 5);        // 7

function makeAdder(n: number): (x: number) => number {
    return x => x + n;
}

const add5 = makeAdder(5);
console.log(add5(3));                            // 8

The pattern is conventional in functional-style TypeScript.

Closures

Functions admit capturing variables from the enclosing scope:

function makeCounter(): () => number {
    let count = 0;
    return () => {
        count++;
        return count;
    };
}

const c = makeCounter();
c();                                              // 1
c();                                              // 2
c();                                              // 3

Closures share the captured variable; each call mutates and observes the same count.

void return type

void indicates “no useful return value”:

function log(message: string): void {
    console.log(message);
}

A subtle subtlety: a function typed () => void admits any return value (which is then ignored at the call site):

type Callback = (n: number) => void;

const cb: Callback = (n) => n * 2;               // OK; the returned value is ignored

This admits using non-void-returning functions as void callbacks; conventional in forEach and similar patterns.

never return type

never indicates “the function does not return normally” — it throws or runs forever:

function fail(message: string): never {
    throw new Error(message);
}

function infiniteLoop(): never {
    while (true) {}
}

function processOrFail(x: unknown): string {
    if (typeof x !== "string") {
        fail(`Expected string, got ${typeof x}`);  // narrows out non-string
    }
    return x;                                     // narrowed to string
}

The compiler uses never returns for narrowing — code after a never-returning call is unreachable, so type narrowing applies.

Async functions

The async keyword marks an async function — returning a Promise:

async function fetchUser(id: string): Promise<User> {
    const response = await fetch(`/api/users/${id}`);
    return response.json();
}

const user = await fetchUser("123");

Treated in Async and concurrency.

Generator functions

The function* syntax produces a generator:

function* range(start: number, end: number): Generator<number> {
    for (let i = start; i < end; i++) {
        yield i;
    }
}

for (const n of range(0, 5)) {
    console.log(n);                               // 0, 1, 2, 3, 4
}

Treated in Loops.

Common patterns

Default parameters

function fetch(
    url: string,
    options: { method?: string; headers?: Record<string, string> } = {},
): Promise<Response> {
    return globalThis.fetch(url, {
        method: options.method ?? "GET",
        headers: options.headers ?? {},
    });
}

Builder pattern with method chaining

class QueryBuilder {
    private filters: string[] = [];
    private orderBy?: string;

    where(condition: string): this {
        this.filters.push(condition);
        return this;
    }

    orderByField(field: string): this {
        this.orderBy = field;
        return this;
    }

    build(): string {
        let q = "SELECT *";
        if (this.filters.length > 0) {
            q += ` WHERE ${this.filters.join(" AND ")}`;
        }
        if (this.orderBy) q += ` ORDER BY ${this.orderBy}`;
        return q;
    }
}

const sql = new QueryBuilder()
    .where("active = true")
    .where("role = 'admin'")
    .orderByField("created_at")
    .build();

The this return type admits chained method calls; treated in Classes and OOP.

Variadic with tuples

function fn<T extends unknown[]>(...args: T): T {
    return args;
}

const a = fn(1, "hello", true);                  // [number, string, boolean]

Function composition

function compose<A, B, C>(f: (a: A) => B, g: (b: B) => C): (a: A) => C {
    return (a: A) => g(f(a));
}

const inc = (n: number) => n + 1;
const double = (n: number) => n * 2;
const incThenDouble = compose(inc, double);
console.log(incThenDouble(5));                   // 12

For multi-argument composition, libraries (fp-ts, Ramda) admit substantial functional patterns.

Curried function

function curry<A, B, C>(fn: (a: A, b: B) => C): (a: A) => (b: B) => C {
    return (a: A) => (b: B) => fn(a, b);
}

const add = (a: number, b: number) => a + b;
const curriedAdd = curry(add);
const add5 = curriedAdd(5);
console.log(add5(3));                            // 8

Memoization

function memoize<A extends unknown[], R>(fn: (...args: A) => R): (...args: A) => R {
    const cache = new Map<string, R>();
    return (...args: A) => {
        const key = JSON.stringify(args);
        if (!cache.has(key)) {
            cache.set(key, fn(...args));
        }
        return cache.get(key)!;
    };
}

const slowFib = (n: number): number =>
    n < 2 ? n : slowFib(n - 1) + slowFib(n - 2);

const fastFib = memoize((n: number): number =>
    n < 2 ? n : fastFib(n - 1) + fastFib(n - 2));

Async retry

async function retry<T>(
    fn: () => Promise<T>,
    attempts = 3,
    delay = 100,
): Promise<T> {
    let lastError: unknown;
    for (let i = 0; i < attempts; i++) {
        try {
            return await fn();
        } catch (e) {
            lastError = e;
            if (i < attempts - 1) {
                await new Promise(r => setTimeout(r, delay * (i + 1)));
            }
        }
    }
    throw lastError;
}

const data = await retry(() => fetch("/api").then(r => r.json()));

Partial application

function partial<A, B, C>(
    fn: (a: A, b: B) => C,
    a: A,
): (b: B) => C {
    return (b: B) => fn(a, b);
}

const greet = (greeting: string, name: string) => `${greeting}, ${name}`;
const sayHello = partial(greet, "Hello");
console.log(sayHello("Alice"));                  // "Hello, Alice"

Type-safe event handlers

type EventHandlers = {
    click: (e: MouseEvent) => void;
    keypress: (e: KeyboardEvent) => void;
    scroll: (offset: number) => void;
};

function on<K extends keyof EventHandlers>(
    event: K,
    handler: EventHandlers[K],
): void {
    /* ... */
}

on("click", e => console.log(e.clientX));
on("keypress", e => console.log(e.key));
on("scroll", offset => console.log(offset));

Function type with optional this

interface Animal {
    name: string;
    speak(this: Animal): string;
}

const cat: Animal = {
    name: "Whiskers",
    speak() {
        return `${this.name} says meow`;
    },
};

console.log(cat.speak());                        // "Whiskers says meow"

Generic factory

function create<T>(Constructor: new () => T): T {
    return new Constructor();
}

class Logger {}
const logger = create(Logger);                    // type: Logger

A note on the conventional discipline

The contemporary TypeScript function advice:

  • Use arrow functions for callbacks and concise expressions.
  • Use function declarations for top-level functions (admits hoisting and named stack traces).
  • Type the parameters always; type the return type for exported functions.
  • Use defaults and optionals over manual undefined checks.
  • Use rest parameters for variadic functions.
  • Use generics for genuinely generic functions.
  • Use overloads sparingly — when the return type depends on the argument types.
  • Use void for “no useful return”; use never for “doesn’t return”.
  • Prefer immutable parameter types (readonly arrays) where applicable.
  • Use closures for state encapsulation.

The combination — function declarations and arrow functions, optional and default parameters, rest parameters, overloads, generics, the this parameter, async and generator forms — is the substance of TypeScript’s function surface. The discipline produces clear, type-safe, expressive function code.