Last 30 Days
No notifications
Functions are where types pay off the most: every parameter is a typed contract, every return is a guarantee. TS lets you describe optional params, default values, rest spreads, function variables, and even multiple call signatures (overloads) for the same function.
function add(a: number, b: number): number {
return a + b;
}Return types can usually be inferred — leave them off for short functions, annotate for public APIs (it stops accidental return-type drift).
const square = (n: number) => n * n; // returns number (inferred)function greet(name: string, title?: string): string {
return title ? Hi ${title} ${name} : Hi ${name};
}function add(a: number, b: number = 0): number {
return a + b;
}
function sum(...nums: number[]): number {
return nums.reduce((a, b) => a + b, 0);
}
> Order: required → optional → default → rest. Optional params come *after* required ones.
type BinaryOp = (a: number, b: number) => number;const multiply: BinaryOp = (a, b) => a * b; // params/return inferred from BinaryOp
That same signature using an interface (call signature):
interface BinaryOp {
(a: number, b: number): number;
}void vs undefinedvoid means *the caller shouldn't rely on the return value*. undefined means the function actually returns undefined. They look similar but differ in callback contexts:
type Logger = (msg: string) => void;// All of these are valid Loggers — TS doesn't care that they return something:
const a: Logger = (m) => console.log(m); // returns undefined
const b: Logger = (m) => "logged: " + m; // returns string — STILL OK!
That's why Array.prototype.forEach accepts (item) => true — the callback's return is discarded.
unknown parameters & guardsfunction size(value: unknown): number {
if (typeof value === "string" || Array.isArray(value)) return value.length;
if (value instanceof Set || value instanceof Map) return value.size;
return 0;
}When a function behaves differently depending on the *types* of its inputs, write overload signatures above the implementation:
function reverse(s: string): string;
function reverse<T>(arr: T[]): T[];
function reverse(input: string unknown[]): string
unknown[] {
if (typeof input === "string") return input.split("").reverse().join("");
return [...input].reverse();
}reverse("abc"); // string
reverse([1, 2, 3]); // number[]
The implementation signature is not visible to callers — only the overloads are.
> If a single union signature works (function reverse), prefer that over overloads. Overloads are best when input/output types are *correlated*.
this parameterYou can declare this as a fake first parameter — TS won't pass it, but it'll type-check the binding.
interface User { name: string }
function greet(this: User) { return Hi ${this.name}; }
greet.call({ name: "Ada" }); // ✅
// greet(); // ❌ this is implicitly Window/undefinedinterface Greeter {
(name: string): string; // call signature
defaultGreeting: string; // properties on the function itself
}const hello: Greeter = ((n: string) => Hi ${n}) as Greeter;
hello.defaultGreeting = "Hi";
You'll cover generics in depth later, but here's the teaser — they let you connect input and output types:
function first<T>(arr: T[]): T | undefined {
return arr[0];
}const n = first([1, 2, 3]); // number | undefined
const s = first(["a", "b"]); // string | undefined
1. Annotate parameters; let TS infer returns (except on exported APIs).
2. Use readonly arrays for parameters you won't mutate.
3. Prefer union signatures over overloads when possible.
4. Use void for callback returns you don't care about.
5. Never use Function as a type — it's the equivalent of any for functions.