Technology  /  TypeScript

๐Ÿ”ท TypeScript 16 guides ยท updated 2026

Type erasure, structural typing, generics, and the type-level programming toolkit โ€” TypeScript taught the way it actually behaves at compile time and at runtime.

Functions in TypeScript: Parameters, Overloads, and Typing this Correctly

Typing a functionโ€™s parameters and return value looks trivial โ€” a couple of colons, done. Then you hit a function that behaves differently depending on its arguments, or a method that needs to know what this refers to, and suddenly โ€œjust add typesโ€ stops being obvious. This guide covers function typing at the depth real codebases actually need: optional and default parameters, rest parameters, function types as values, overloads, and the this parameter that trips up nearly everyone once.


Basic Parameter and Return Typing

function add(a: number, b: number): number {
return a + b;
}
// Return type is almost always inferable โ€” annotate it anyway for public APIs
function multiply(a: number, b: number) {
return a * b; // inferred as number
}

Parameters must always be annotated (thereโ€™s no value to infer from at definition time), while return types are usually optional to write explicitly โ€” TypeScript infers them from what the function body actually returns. The exception worth knowing: for functions that are part of a public API (exported from a module, used by other developers), writing the return type explicitly is good practice anyway โ€” it acts as a contract that catches you if a later refactor accidentally changes what the function returns, rather than silently propagating the new inferred type to every caller.


Optional and Default Parameters

function greet(name: string, greeting?: string) {
return `${greeting ?? "Hello"}, ${name}!`;
}
greet("Alice"); // "Hello, Alice!"
greet("Alice", "Hey"); // "Hey, Alice!"
function greetWithDefault(name: string, greeting: string = "Hello") {
return `${greeting}, ${name}!`;
}

greeting?: string makes the parameter optional and its type becomes string | undefined inside the function โ€” you have to handle the undefined case explicitly (as the ?? above does). greeting: string = "Hello" is subtly different: the parameterโ€™s type inside the function body is just string, because TypeScript knows the default value fills in for undefined before the function body ever runs. Reach for a default value whenever thereโ€™s a sensible fallback; reach for ? when โ€œnot providedโ€ needs to be handled as meaningfully distinct from any specific value.

The rule that catches people: required parameters must come before optional ones.

function bad(greeting?: string, name: string) { ... }
// Error: A required parameter cannot follow an optional parameter

This isnโ€™t arbitrary pedantry โ€” it mirrors how JavaScript actually calls functions positionally. If optional parameters could precede required ones, bad("Hi") would be ambiguous: is "Hi" the greeting, or did the caller mean to skip greeting and provide it as name? Ordering resolves the ambiguity by construction.


Rest Parameters

function sum(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0);
}
sum(1, 2, 3, 4); // 24 becomes... wait: 1+2+3+4 = 10
function logWithPrefix(prefix: string, ...messages: string[]) {
messages.forEach(m => console.log(`[${prefix}] ${m}`));
}
logWithPrefix("INFO", "Server started", "Listening on port 3000");

A rest parameter must be the last parameter, and its type annotation describes the type of each individual element, not the array itself โ€” ...numbers: number[] means โ€œhowever many arguments are passed here, each one is a number, and inside the function theyโ€™re collected into an array called numbers.โ€


Function Types as Values

// A type describing "a function that takes two numbers and returns a number"
type MathOperation = (a: number, b: number) => number;
const add: MathOperation = (a, b) => a + b;
const subtract: MathOperation = (a, b) => a - b;
function calculate(a: number, b: number, operation: MathOperation): number {
return operation(a, b);
}
calculate(10, 5, add); // 15
calculate(10, 5, subtract); // 5

Once a functionโ€™s shape is captured as a type (MathOperation here), TypeScript infers the parameter types for any function assigned to it โ€” (a, b) => a + b doesnโ€™t need a: number, b: number written out, because the MathOperation type already told TypeScript what to expect. This is called contextual typing, and itโ€™s why callback parameters in .map(), .filter(), and similar higher-order functions rarely need explicit annotations โ€” the array methodโ€™s own type signature already supplies the context.

const numbers = [1, 2, 3];
numbers.map((n) => n * 2); // n inferred as number โ€” no annotation needed
numbers.map((n: string) => n); // Error: doesn't match the array's actual element type

Function Overloads

Some functions genuinely behave differently depending on whatโ€™s passed in โ€” not just different values, but a different relationship between input and output type. This is what overloads exist for.

// Overload signatures โ€” describe each valid calling pattern
function getElement(selector: string): Element | null;
function getElement(selectors: string[]): (Element | null)[];
// Implementation signature โ€” must be compatible with every overload above, never called directly by consumers
function getElement(selectorOrSelectors: string | string[]): Element | null | (Element | null)[] {
if (Array.isArray(selectorOrSelectors)) {
return selectorOrSelectors.map(s => document.querySelector(s));
}
return document.querySelector(selectorOrSelectors);
}
const single = getElement("#app"); // typed as Element | null
const multiple = getElement([".item"]); // typed as (Element | null)[]

Without overloads, youโ€™d have to write the implementation signatureโ€™s wide union type (string | string[] in, Element | null | (Element | null)[] out) as the only type callers see โ€” which means every caller has to narrow the return value themselves, even when they know exactly which overload applies from the argument they passed. Overloads let each specific calling pattern get its own precise, narrower type, and TypeScript picks the matching overload automatically based on the arguments at the call site.

The implementation signature (the last one, with the actual function body) is never visible to callers โ€” only the overload signatures above it are. This is a common point of confusion: three overloads plus one implementation looks like four functions, but callers only ever see three possible call shapes.

string argument

string[] argument

getElement('#app')

Which overload

signature matches?

Overload 1: returns Element | null

Overload 2: returns (Element | null)[]

Same implementation runs either way

When to actually use overloads vs a union type: if the relationship between a specific input type and its specific output type is straightforward to express with a union and some narrowing inside the function, skip overloads โ€” they add real complexity. Reach for them specifically when the caller benefits from getting back a more precise type than a narrowing check inside a single signature could express (as in the getElement example, where a string input reliably means a single-element return, not a union the caller has to re-check).


Typing this in Functions

interface Button {
text: string;
onClick: (this: Button, event: MouseEvent) => void; // `this` parameter โ€” erased at runtime, checked at compile time
}
const button: Button = {
text: "Submit",
onClick(event) {
console.log(this.text); // TypeScript knows `this` is a Button here
},
};

The this: Button parameter above is a special TypeScript-only construct โ€” it doesnโ€™t become a real parameter in the compiled JavaScript (another instance of type erasure), it exists purely so the compiler can check that this inside the function body is used correctly. Without it, this defaults to any inside a standalone function, silently disabling checking on every property access through it โ€” exactly the kind of bug class strict typing exists to prevent.

// The classic runtime footgun a typed `this` parameter helps surface
function handleClick(this: Button, event: MouseEvent) {
console.log(this.text);
}
const detachedHandler = button.onClick;
detachedHandler(new MouseEvent("click"));
// TypeScript flags this at the call site: "The 'this' context of type 'void' is not assignable to method's 'this' of type 'Button'"

This mirrors a real JavaScript runtime bug โ€” detaching a method from its object and calling it standalone breaks this (covered in more depth in the JavaScript closures material) โ€” but here TypeScript catches the mistake before the code runs, rather than the developer discovering this.text is undefined in production. Arrow functions sidestep this entirely by inheriting this lexically rather than from the call site, which is why arrow functions are the more common choice for callbacks in modern codebases โ€” but understanding the typed this parameter matters for library code (event handlers, plugin APIs) still written with regular functions.


Function Parameter Bivariance and Assignability

type Handler = (event: { type: string }) => void;
const specificHandler: Handler = (event: { type: string; timestamp: number }) => {
// Error: Property 'timestamp' is missing โ€” this direction of assignment IS checked strictly
};
const wideHandler: (event: { type: string; timestamp: number }) => void =
(event: { type: string }) => { console.log(event.type); }; // This direction is generally allowed

Functions expecting a wider parameter type can generally be substituted where a function expecting a narrower one is needed โ€” this mirrors real usage: a generic event handler that only reads event.type can safely be used where a more specific handler was expected, because it only ever touches the properties it knows are guaranteed to exist. This property is called parameter contravariance, and while the details get subtle with strictFunctionTypes enabled (part of strict: true), the practical takeaway is: a function accepting less specific input than required is usually safe to substitute; a function requiring more specific input than whatโ€™s actually available is not.


Async Functions

async function fetchUser(id: string): Promise<{ id: string; name: string }> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
const userPromise = fetchUser("123"); // typed as Promise<{ id: string; name: string }>

An async functionโ€™s declared return type is always wrapped in Promise<T> โ€” writing : { id: string; name: string } instead of : Promise<{ id: string; name: string }> for an async function is a genuine mistake TypeScript will catch, since an async function can never return a bare value synchronously; it always returns a Promise that eventually resolves to one.


Common Function Typing Patterns

Callback with an optional error-first signature (Node.js style)

type NodeCallback<T> = (error: Error | null, result?: T) => void;
function readConfig(path: string, callback: NodeCallback<string>) {
fs.readFile(path, "utf-8", (err, data) => {
if (err) return callback(err);
callback(null, data);
});
}

A function that returns different shapes based on a discriminant argument

function createResponse(success: true, data: unknown): { success: true; data: unknown };
function createResponse(success: false, error: string): { success: false; error: string };
function createResponse(success: boolean, payload: unknown) {
return success ? { success, data: payload } : { success, error: payload };
}

Curried functions with full type inference

function multiply(a: number) {
return function (b: number): number {
return a * b;
};
}
const double = multiply(2);
double(5); // 10 โ€” TypeScript infers the returned function's type from multiply's body

Frequently Asked Questions

Do I need to type every functionโ€™s return value explicitly? No โ€” inference handles it correctly in the vast majority of cases. Annotate return types explicitly on exported/public functions specifically, so a later change to the function body that accidentally alters the inferred return type shows up as a compile error at the function definition, rather than as a confusing error at every call site.

Why does TypeScript let me pass a function expecting fewer parameters where more are expected? JavaScript itself allows calling a function with more arguments than it declares (theyโ€™re simply ignored) โ€” array.forEach((item) => ...) works even though forEachโ€™s callback actually receives (item, index, array). TypeScriptโ€™s function typing reflects this real JavaScript behavior rather than fighting it.

Whatโ€™s the actual difference between an overload and a union-typed parameter? A union parameter (function f(x: string | number)) gives callers back one type regardless of which branch of the union they passed in โ€” the caller has to narrow the return value themselves if it also varies. Overloads let each specific input shape map to its own specific, more precise output shape automatically, at the cost of more declaration boilerplate.

Should I use arrow functions or regular functions for methods? For object methods and callbacks where this should behave predictably, arrow functions (inheriting this lexically) usually cause fewer surprises. Regular functions remain necessary specifically when you need dynamic this binding โ€” class methods, and any API (like the Button.onClick example above) explicitly designed around a typed this parameter.

Can I overload arrow functions the way I overloaded getElement above? Not directly with the same syntax โ€” arrow function overloads require declaring the overload signatures as a type or interface first, then assigning a single implementation to it, since arrow functions donโ€™t support the sequential-declaration overload syntax that function declarations do.


Whatโ€™s Next

Youโ€™ve now covered TypeScriptโ€™s core function-typing toolkit: optional/default/rest parameters, function types as values, overloads for genuinely varying signatures, and the this parameter that catches a real class of runtime bugs at compile time. This closes out the fundamentals section.

Next: Interfaces in TypeScript, where we move from typing individual values and functions to typing the shape of entire objects โ€” the foundation for everything in the type system section that follows.