Functions and Objects
Functions and object shapes are where you’ll spend most of your annotation budget. This page covers both, plus the structural rules that decide whether one type fits another.
Typing functions
Section titled “Typing functions”A function type has parameter types and a return type.
function add(a: number, b: number): number { return a + b;}
const multiply = (a: number, b: number): number => a * b;The return type can almost always be inferred, so it’s optional:
function add(a: number, b: number) { return a + b; // inferred: number}Parameters can never be inferred from the body — annotate them, always. With noImplicitAny on,
the compiler tells you so:
function add(a, b) { return a + b; }// error TS7006: Parameter 'a' implicitly has an 'any' type.Function type expressions
Section titled “Function type expressions”To describe a function as a value — a callback parameter, a stored handler — use the arrow syntax:
type Transform = (input: string) => string;
function apply(value: string, fn: Transform): string { return fn(value);}
apply('abc', (s) => s.toUpperCase()); // `s` is contextually typed as stringParameter names in a function type are documentation only; only positions and types matter.
(a: string) => void and (input: string) => void are the same type.
Optional, default, and rest parameters
Section titled “Optional, default, and rest parameters”// Optional: callers may omit it. Type inside the body is `string | undefined`.function greet(name: string, title?: string) { return title ? `${title} ${name}` : name;}
// Default: also optional for the caller, but never undefined inside the body.function repeat(text: string, times = 2) { return text.repeat(times); // `times` is number}
// Rest: collects the remaining arguments into an array (or a tuple).function sum(...nums: number[]): number { return nums.reduce((a, b) => a + b, 0);}sum(1, 2, 3); // => 6Rules worth knowing:
- Optional parameters must come after required ones. A default parameter may sit anywhere, but
callers can only skip it by passing
undefined. name?: stringis not the same asname: string | undefined. The first lets callers omit the argument entirely; the second requires them to pass something, even if it’sundefined.- A rest parameter must be last, and its type must be an array or tuple type.
function tag(...parts: [string, number, ...boolean[]]) {}tag('a', 1, true, false); // finetag('a'); // error: Expected at least 2 argumentsthis typing
Section titled “this typing”For functions that rely on a dynamic this, declare it as a fake first parameter. It is erased
at compile time.
interface Button { label: string; onClick(this: Button, event: Event): void;}Arrow functions have no this of their own, so this doesn’t apply to them.
void returns and callbacks
Section titled “void returns and callbacks”void means “the caller must ignore whatever comes back”, not “returns undefined”. A callback
that returns a value is assignable to a void-returning parameter type:
const seen: number[] = [];[1, 2, 3].forEach((n) => seen.push(n)); // push returns number; acceptedWithout this rule, half of all real-world callbacks would fail to type-check.
Overloads
Section titled “Overloads”When one function has genuinely different signatures depending on its arguments, write overload signatures followed by a single implementation signature.
function parse(input: string): object;function parse(input: string, reviver: (k: string, v: unknown) => unknown): object;function parse(input: Buffer): object;function parse(input: string | Buffer, reviver?: (k: string, v: unknown) => unknown): object { const text = typeof input === 'string' ? input : input.toString('utf8'); return JSON.parse(text, reviver);}Rules:
- The implementation signature is not callable. Only the overloads above it are visible to callers.
- The implementation signature must be compatible with every overload, which usually means unions and optional parameters.
- Overloads are tried top to bottom; the first match wins. Put more specific ones first.
function len(x: string): number;function len(x: unknown[]): number;function len(x: string | unknown[]): number { return x.length;}
len('abc'); // 3len([1, 2]); // 2len(42); // error: No overload matches this callOverloads that differ only by return type based on an argument are usually better expressed with a conditional type or a generic:
// Instead of two overloads:function first<T>(items: T[]): T | undefined { return items[0];}Constructor signatures
Section titled “Constructor signatures”To type “something you can new”:
type Ctor<T> = new (...args: any[]) => T;
function create<T>(C: Ctor<T>): T { return new C();}
class Widget {}const w = create(Widget); // WidgetObject types
Section titled “Object types”Describe an object by listing its members:
function print(user: { id: number; name: string }): void { console.log(user.id, user.name);}Inline object types get unreadable quickly, so name them.
Optional properties
Section titled “Optional properties”interface Options { url: string; timeout?: number; // may be absent; type inside is number | undefined retries?: number;}
function request(opts: Options) { const timeout = opts.timeout ?? 5000; // handle the undefined}
request({ url: '/api' }); // finereadonly properties
Section titled “readonly properties”interface Config { readonly apiKey: string; readonly endpoints: string[];}
const c: Config = { apiKey: 'k', endpoints: ['/a'] };c.apiKey = 'other'; // error: Cannot assign to 'apiKey' because it is a read-only propertyc.endpoints.push('/b'); // ALLOWED — readonly is shallowreadonly is a compile-time marker only; nothing is frozen at runtime. It is also not checked
during assignability, which surprises people:
interface Mutable { x: number }interface Frozen { readonly x: number }
let m: Mutable = { x: 1 };let f: Frozen = m; // allowedf.x = 2; // error via `f`... but `m.x = 2` still worksTreat readonly as intent and local protection, not a guarantee.
Index signatures
Section titled “Index signatures”When keys aren’t known ahead of time:
interface StringMap { [key: string]: string;}
const headers: StringMap = { 'content-type': 'application/json' };headers['x-custom'] = 'value';headers.anything; // typed stringKey type may be string, number, symbol, or a template literal type. Named properties must be
compatible with the index signature:
interface Mixed { [key: string]: number; length: number; // fine name: string; // error: Property 'name' of type 'string' is not assignable to // 'string' index type 'number'}Fix that by widening the index signature to number | string, or by using two separate types.
Record<K, V> is usually nicer than writing the index signature by hand, and it supports finite
key sets:
type Headers = Record<string, string>;type Flags = Record<'debug' | 'verbose', boolean>; // both keys requiredExcess property checks
Section titled “Excess property checks”Structural typing says extra properties are fine — except when you assign an object literal directly. Then TypeScript flags unknown properties, on the theory that you made a typo.
interface Point { x: number; y: number }
const p: Point = { x: 1, y: 2, z: 3 };// error: Object literal may only specify known properties,// and 'z' does not exist in type 'Point'.
// But via a variable, it's accepted:const raw = { x: 1, y: 2, z: 3 };const q: Point = raw; // fine — `raw` is not a fresh literalThis “freshness” check applies to direct assignments, arguments, and returned literals. Ways around it when the extra property is intentional:
const p = { x: 1, y: 2, z: 3 } as Point; // assertion (blunt)const p: Point & { z: number } = { x: 1, y: 2, z: 3 }; // widen the typeinterface Point { x: number; y: number; [k: string]: unknown } // allow extrasinterface vs type alias
Section titled “interface vs type alias”The two overlap heavily. The real differences:
interface |
type alias |
|
|---|---|---|
| Object shapes | Yes | Yes |
| Unions, tuples, primitives | No | Yes |
| Mapped / conditional types | No | Yes |
| Declaration merging | Yes | No |
| Extending | extends |
& intersection |
| Implemented by classes | Yes | Yes (if object-shaped) |
| Error messages | Often shorter (name preserved) | Sometimes expanded |
// Only `type` can do these:type ID = string | number;type Pair = [number, number];type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] };Declaration merging
Section titled “Declaration merging”Declare an interface twice and the declarations combine. This is how you extend types owned by someone else.
interface Window { myAppVersion: string;}// Now `window.myAppVersion` type-checks anywhere in the project.type A = { a: number };type A = { b: number }; // error: Duplicate identifier 'A'Merging is a feature for library augmentation and a hazard for application code — two files can silently add fields to the same interface. See classes and declarations for the module-augmentation form.
extends vs &
Section titled “extends vs &”They look equivalent but differ on conflicts.
interface A { x: number }interface B extends A { x: string }// error: Interface 'B' incorrectly extends interface 'A'.
type C = { x: number } & { x: string };// No error at the declaration. `C['x']` is `number & string` = never.const c: C = { x: 1 }; // error only here, and the message is worseextends fails fast at the declaration site with a clear message. Intersections defer the problem.
Structural typing in depth
Section titled “Structural typing in depth”TypeScript compares types by structure, not by name. Two independently declared types with the same members are interchangeable.
interface Duck { quack(): void }class Robot { quack() { console.log('beep'); } }
function makeItQuack(d: Duck) { d.quack(); }makeItQuack(new Robot()); // fine — Robot has the right shapeThe assignability rule for objects: S is assignable to T if S has, at minimum, every
member T requires, with compatible types. Extra members are fine (except for the freshness
check above).
interface Named { name: string }const person = { name: 'Ada', age: 36 };const n: Named = person; // fine — extra `age` is ignoredThis is exactly what you want for JavaScript interop and for functions that accept “anything with
a .length”.
Where structural typing bites
Section titled “Where structural typing bites”Two types that mean different things but share a shape are interchangeable:
type UserId = string;type OrderId = string;
function getUser(id: UserId) {}const orderId: OrderId = 'ord_123';getUser(orderId); // no error — both are just `string`If you need to keep them apart, use branded types, covered in patterns and gotchas.
The one nominal exception
Section titled “The one nominal exception”Classes with private or protected members are compared nominally — the members must originate
from the same declaration.
class A { private secret = 1 }class B { private secret = 1 }
let a: A = new B(); // error: Types have separate declarations of a private property 'secret'Same for the ECMAScript #private fields.
Key points
Section titled “Key points”- Annotate parameters always; let return types be inferred except at public boundaries.
param?: Tallows omission;param: T | undefinedrequires an explicit argument.voidreturn types accept functions that return values — this makes callbacks work.- Overloads hide the implementation signature; prefer unions or generics when possible.
readonlyis shallow, compile-time only, and ignored during assignability.- Index signatures over-promise; pair them with
noUncheckedIndexedAccessor useMap. - Object literals get excess property checks; variables don’t.
- Use
interfacefor extensible object shapes,typefor everything else. - Assignability is structural — shape decides — except for
private/protectedclass members.