Capítulo 4 de 36

Chapter 4: More on Functions

Core Idea

Functions are values with their own type syntax layer: how to type a callable (with or without properties, with or without new), how generics relate parameter and return types, and the special-case types (void, object, unknown, never, Function) that show up specifically around function signatures.

Key Concepts

  • Function type expressions: (a: string) => void — arrow-like syntax, parameter names are required in the type ((string) => void means a parameter literally named string of type any, not what it looks like). Name it via type GreetFunction = (a: string) => void.
  • Call signatures: a function type expression can't carry extra properties. To type something both callable and propertied, use an object type with a signature (someArg: number): boolean (note : not =>) alongside other properties.
  • Construct signatures: new (s: string): SomeObject types something invocable with new. Call and construct signatures can coexist on the same type (e.g. Date, callable both ways).
  • Generics: a type parameter (<Type>) links an input type to an output/another input type — function firstElement<Type>(arr: Type[]): Type | undefined. TypeScript infers Type from the call site in most cases; multiple parameters (<Input, Output>) are fine too.
  • Constraints (<Type extends { length: number }>): restrict what a type parameter can be, unlocking property access inside the function body. A constrained generic still returns the specific input type, not just something matching the constraint — writing a function that returns a constraint-shaped literal instead of the actual input Type is a type error (correctly so: the caller was promised back the same kind of object they passed in).
  • Explicit type arguments (combine<string | number>(a, b)): needed when TS can't unify mismatched call-site types on its own.
  • Guidelines for generics (the "red flags"):
    1. Push type parameters down — use the parameter itself rather than constraining it when possible (<Type>(arr: Type[]) infers a real return type; <Type extends any[]>(arr: Type) degrades to any).
    2. Use fewer type parameters — a type parameter that doesn't relate two or more values (e.g. one only used to type a single callback argument) is pure friction for callers.
    3. A type parameter should appear at least twice — if it shows up in only one place (params or return, combined), you don't need generics at all; a concrete type is simpler and equally correct.
  • Optional parameters (x?: number): the parameter's actual type inside the body is T | undefined. A default (x = 10) instead gives the body a non-optional T, since undefined args get replaced. Callers may always explicitly pass undefined to an optional param.
  • Optional parameters in callback types are a common trap: writing callback: (arg: any, index?: number) => void doesn't mean "callers may omit index" — it means "the implementation may choose to invoke the callback with fewer args", so TS will reject code inside that callback that unconditionally uses index as a number. Rule: never write an optional parameter in a callback type unless you intend to call it without that argument. (JS just ignores extra call-site arguments, so a plain, fully-typed parameter list is almost always what you want instead.)
  • Function overloads: multiple overload signatures followed by one broader, non-callable-from-outside implementation signature — lets one function name accept genuinely different argument-count/type combinations (e.g. makeDate(timestamp) vs makeDate(m, d, y)). The implementation signature must be compatible with (a superset covering) every overload; it is invisible to callers, so writing only the implementation signature with optional params does not make it callable with fewer args unless overloads say so explicitly.
  • Prefer a union-typed parameter over overloads when the overloads have the same arity/return type — overloads can't be resolved against a value that's itself a union at the call site (len(cond ? "a" : [1]) fails even though both len(string) and len(any[]) overloads exist individually).
  • this parameters: JS forbids a real parameter named this, so TypeScript repurposes that syntax slot to declare the function's expected this type — function (this: User) { return this.admin }. Required for callback-style APIs where the caller controls this binding; only works with function, not arrow functions (arrows lexically capture this and can't redeclare it).
  • Special function-adjacent types:
    • void: inferred return type when a function has no return, or bare return; — distinct from undefined even though a void JS function runtime-returns undefined.
    • object: any non-primitive (excludes string/number/bigint/boolean/symbol/null/undefined) — not the same as the near-useless global Object, and not the same as the empty-object-type {}. Function values count as object.
    • unknown: the type-safe counterpart to any — accepts any value, but nothing can be done with an unknown value until it's narrowed/asserted. Good for "accepts anything" parameters and "returns some value the caller must check" returns (e.g. JSON.parse wrapped as unknown).
    • never: return type of a function that always throws or never terminates; also what a fully-narrowed-away union collapses to.
    • Function: the global type exposing bind/call/apply; calling a Function-typed value returns any (an "untyped function call" — usually avoid; prefer () => void if you just need "any callable, not intending to invoke it").
  • Rest parameters (...m: number[]): implicitly any[] if unannotated; explicit annotation must be T[]/Array<T>/a tuple type.
  • Rest arguments / spread: TS does not assume arrays are immutable — spreading a plain number[] into a fixed-arity function like Math.atan2(...args) fails type-checking because the array's length isn't statically known. Fix with as const (freezes it to a fixed-length tuple).
  • Parameter destructuring: annotate after the destructuring pattern — function sum({ a, b, c }: { a: number; b: number; c: number }), or via a named type alias for readability.
  • Void-returning function assignability (a frequent surprise): a variable/parameter typed () => void accepts an implementation that does return a value (e.g. const f: voidFunc = () => true is legal — the return value is just ignored/typed as void on the caller's side). This is deliberate, so patterns like arr.forEach(el => dst.push(el)) type-check even though push returns number. The exception: a function declared literally as function f(): void { return true } (return type written directly on the function, not via a variable's contextual type) is a hard error — that specific form does enforce "must not return a value".

Code Examples

function longest<Type extends { length: number }>(a: Type, b: Type) {
  return a.length >= b.length ? a : b;
}
const longerArray = longest([1, 2], [1, 2, 3]);   // number[]
const longerString = longest("alice", "bob");      // "alice" | "bob"
  • What it demonstrates: a generic constrained by a shape ({ length: number }) rather than a specific type — usable across arrays and strings alike, while still returning the caller's exact input type.
function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
  return d !== undefined && y !== undefined ? new Date(y, mOrTimestamp, d) : new Date(mOrTimestamp);
}
  • What it demonstrates: function overloads — two public call shapes backed by one broader implementation signature that callers never see directly.

Reference Tables

TypeMeaningContrast
voidinferred no-return / explicit "returns nothing"not the same as undefined
objectany non-primitivenot Object (global type, rarely useful), not {}
unknownanything, but unusable until narrowedsafer alternative to any
nevera value that can't occur (throws, infinite loop, exhausted union)assignable to everything; nothing (but never) assignable to it
Functionany callable (has bind/call/apply)calling it returns any — usually avoid

Key Takeaways

  1. Never write an optional parameter on a callback type unless the implementation is actually allowed to omit that argument when invoking it — it changes what the type promises, not just what's convenient to call.
  2. A type parameter used only once (params+return combined) isn't relating anything — drop the generic and use a concrete type.
  3. Prefer union-typed parameters over overloads whenever the overloads share arity/return shape; overloads can't resolve a union-typed argument at the call site.
  4. unknown is almost always the right choice over any for "accepts/returns something the caller must check" — it forces narrowing before use instead of silently disabling checks.
  5. Freeze array literals with as const before spreading them into fixed-arity calls (Math.atan2(...args)) — otherwise TS only knows "array of N", not the exact length.

Connects To

  • Narrowing: unknown and never both interact directly with narrowing/exhaustiveness checking.
  • Type Manipulation / Generics: this chapter's generics section is the entry point; the dedicated Generics reference chapter goes deeper (defaults, generic classes/interfaces).
  • Classes: this parameter typing extends into method definitions and this-based type guards.