Functions in TypeScript are First-Class Citizens. They can be passed as arguments, returned from other functions, and assigned to variables.
Function typing represents a formal mathematical contract:
f: (A1, A2, ...An) -> R
The contract defines a set of input parameters and a guarantee of the return value's type.
Unlike many statically typed languages, functions in TypeScript have unique properties:
strictFunctionTypes flag is enabled.void vs undefined vs never): The distinction between functions with no useful result, returning undefined, and functions that never return control (infinite loops, throwing exceptions).Let's look at the differences between void, undefined, and never:
+-------------------------------------------------------------------------+
| RETURN SEMANTICS: VOID vs UNDEFINED vs NEVER |
+-------------------------------------------------------------------------+
| void: |
| "I don't care about the result." The function performs a side effect. |
| In JS, it returns undefined, but TypeScript allows callbacks to return |
| any value, ignoring it on the caller's side. |
+-------------------------------------------------------------------------+
| undefined: |
| "The function explicitly returns the value undefined." |
| Requires `return undefined` or an empty `return`. |
+-------------------------------------------------------------------------+
| never: |
| "The function will NEVER complete normally." |
| Either throws an error (`throw new Error()`) or gets stuck in a `while` loop.|
+-------------------------------------------------------------------------+The calling function can pass fewer arguments than declared, but not more than required (arity check).
Each parameter of the target function must safely accept the type of the corresponding argument from the source function.
The return type of the source function must be a subtype of the declared return type of the target signature.
The compiler checks call arguments against overloads from top to bottom until the first complete match is found.
Why does the compiler allow the code `const fn: () => void = () => 42;` to pass type checking successfully, even though the arrow function returns a number?
(event: React.MouseEvent<HTMLButtonElement>) => void.async function fetchEntity(): Promise<Entity> to protect against implicit Promise<unknown>.readFile(path, 'utf-8'): string vs readFile(path): Buffer).A function signature can be declared as an arrow type or within an interface body:
// 1. Arrow type alias:
type FormValidator = (value: string, minLength?: number) => boolean;
// 2. Object type with a Call Signature:
interface Logger {
// Call Signature
(message: string, level?: 'info' | 'warn' | 'error'): void;
// Static property attached to the function
defaultLevel: 'info' | 'warn' | 'error';
}
const consoleLogger: Logger = Object.assign(
(msg: string, level: 'info' | 'warn' | 'error' = 'info') => {
console.log(`[${level.toUpperCase()}] ${msg}`);
},
{ defaultLevel: 'info' as const }
);An overload consists of one or more overload signatures (without a body) and a single implementation signature (with a body):
// Public overloads (visible to API consumers)
function formatTimestamp(timestamp: number): string;
function formatTimestamp(date: Date, locale: string): string;
// Implementation signature (NOT directly visible to consumers!)
function formatTimestamp(arg1: number | Date, arg2?: string): string {
if (typeof arg1 === 'number') {
return new Date(arg1).toISOString();
}
return arg1.toLocaleDateString(arg2 ?? 'en-US');
}
// Correct calls:
formatTimestamp(1728000000000); // OK
formatTimestamp(new Date(), 'ru-RU'); // OK
// ERROR: The compiler only checks against public overloads, not the implementation!
// formatTimestamp(new Date()); // TS2554: Expected 2 arguments, but got 1.Optional parameters (param?: Type) must strictly follow required parameters:
// Default parameters automatically make the parameter optional
function createRequest(
url: string,
timeoutMs: number = 5000,
headers?: Record<string, string>
) {
return { url, timeoutMs, headers: headers ?? {} };
}
// Typed Rest parameters using tuples:
type LogArgs = [code: number, message: string, ...details: unknown[]];
function emitAuditLog(...args: LogArgs): void {
const [code, message, ...details] = args;
console.log(`Audit #${code}: ${message}`, details);
}
emitAuditLog(403, 'Forbidden', { ip: '127.0.0.1' }); // CorrectWhy can't calling code invoke a function with arguments that match the implementation signature if that combination is not present in the public overloads?
TypeScript matches overloads from top to bottom. If a broader signature is placed first, it will intercept all calls:
// ERROR: The first overload will block the second!
function get(url: string): unknown;
function get(url: 'https://api.wpskills.dev'): SpecificResponse;
// CORRECT: Place specific signatures FIRST, and broader ones last!
function get(url: 'https://api.wpskills.dev'): SpecificResponse;
function get(url: string): unknown;Declaring a function with never if it completes normally:
// ERROR: TS2534: A function returning 'never' cannot have a reachable end point.
function logError(msg: string): never {
console.error(msg);
// No throw or while(true)!
}Use void for functions that perform a side effect.