Functions, Signatures & Generics2026-04-04
These notes cover how TypeScript models JavaScript functions, including call signatures, construct signatures, generics, overloads, this, rest syntax, and void behavior.
Call Signatures
In JavaScript, functions are objects, so we can attach properties to them.
The issue is that a plain function type like (arg: number) => boolean only describes inputs and output. It does not describe extra properties on the function object.
Use a call signature inside an object type when you need both:
- object properties
- callable function behavior
type DescribableFunction = {
description: string;
(someArg: number): boolean;
};
Important syntax difference:
- function type expression uses
=> - call signature in an object uses
:
Construct Signatures
Some JavaScript functions are meant to be called with new to construct objects.
A construct signature adds new to the signature:
type SomeConstructor = {
new (s: string): SomeObject;
};
Some APIs can be called with or without new (for example, Date). TypeScript can model that hybrid style:
interface CallOrConstruct {
(n?: number): string;
new (s: string): Date;
}
Generics for Precise Return Types
Using any[] loses type precision:
function firstElement(arr: any[]) {
return arr[0];
}
Use a generic so the return type tracks the array element type:
function firstElement<Type>(arr: Type[]): Type | undefined {
return arr[0];
}
Generic Constraints with extends
Use extends when a generic must have specific properties.
function longest<Type extends { length: number }>(a: Type, b: Type) {
if (a.length >= b.length) {
return a;
}
return b;
}
const longerArray = longest([1, 2], [1, 2, 3]);
const longerString = longest("alice", "bob");
const notOK = longest(10, 100); // Error: number has no length
Prefer Fewer Type Parameters
If a type parameter does not relate multiple values, it is often unnecessary.
function filter1<Type>(arr: Type[], func: (arg: Type) => boolean): Type[] {
return arr.filter(func);
}
function filter2<Type, Func extends (arg: Type) => boolean>(
arr: Type[],
func: Func
): Type[] {
return arr.filter(func);
}
filter1 is usually better because Func adds complexity without adding useful relationships.
Function Overloads
Use overloads when a function supports multiple valid argument shapes.
function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
if (d !== undefined && y !== undefined) {
return new Date(y, mOrTimestamp, d);
}
return new Date(mOrTimestamp);
}
const d1 = makeDate(12345678);
const d2 = makeDate(5, 5, 5);
const d3 = makeDate(1, 3); // Error: no overload with 2 arguments
The last signature is the implementation signature. It is not visible to callers, and it must be compatible with all overload signatures above it.
Declaring this in TypeScript
In callbacks, TypeScript may need help understanding what this points to.
You can annotate this by adding a fake first parameter:
interface User {
admin: boolean;
}
interface DB {
filterUsers(filter: (this: User) => boolean): User[];
}
declare function getDB(): DB;
const db = getDB();
const admins = db.filterUsers(function (this: User) {
return this.admin;
});
Use function, not arrow syntax, for this pattern. Arrow functions capture this from surrounding scope.
Rest Parameters vs Rest Arguments
Both use ..., but they do opposite jobs.
Rest parameters gather multiple arguments into one array inside a function definition:
function multiply(multiplier: number, ...numbers: number[]) {
return numbers.map((x) => x * multiplier);
}
multiply(10, 1, 2, 3, 4);
Spread (rest arguments) unpacks an existing array into separate arguments at call time:
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
arr1.push(...arr2);
The as const Tuple Fix
When spreading into functions that expect a fixed number of arguments, TypeScript may reject plain arrays.
const args = [8, 5];
const angle = Math.atan2(...args); // Error
Use as const to lock the array as a tuple with fixed length:
const args = [8, 5] as const;
const angle = Math.atan2(...args); // OK
The Optional Callback Trap
When defining callback types, it can feel natural to make parameters optional with ? if users might not need them.
// Bad: index? means the callback might be called without index
function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
for (let i = 0; i < arr.length; i++) {
callback(arr[i], i);
}
}
TypeScript interprets index? as: the caller might receive undefined for that argument.
myForEach([1, 2, 3], (a, i) => {
console.log(i.toFixed()); // Error: 'i' is possibly 'undefined'
});
Rule: only use ? in callback parameter types if your implementation genuinely calls the callback without that argument.
// Good: define exactly what your implementation passes
function myForEach(arr: any[], callback: (arg: any, index: number) => void) {
for (let i = 0; i < arr.length; i++) {
callback(arr[i], i);
}
}
JavaScript ignores extra parameters naturally, so users can still write (a) => console.log(a) even when the callback type includes (arg, index).
void Return Type Behavior
A function type like () => void means callers should ignore any returned value.
That is why callback-heavy JavaScript patterns still work:
const src = [1, 2, 3];
const dst: number[] = [];
src.forEach((el) => dst.push(el));
Even though push returns a number, forEach accepts a callback typed with void return.
However, if you explicitly annotate a function declaration with : void, TypeScript enforces no returned value:
function f2(): void {
return true; // Error
}