Functions
A function in TypeScript is a body plus a signature, and the whole topic comes down to what the compiler does with the signature and what it cannot. The signature is not documentation: it is checked at each side of a call, it drives inference for callback parameters (contextual typing — you leave the arrow's parameters inside map unannotated not out of laziness but because their type is already fixed by the target), and it is erased entirely before the JavaScript ships. Hence the dividing line of the topic: everything you declare about a function is a promise to the compiler, not code that will run.
Every trap in the topic follows from that erasure. Overloads are a set of caller-visible signatures over one implementation: no type-based dispatch appears at runtime, you write the branching by hand, and the implementation signature is not even callable. The this parameter is a fake first parameter that is not an argument: it only types the receiver and disappears on emit, and an arrow function has none at all because its this is captured lexically. A generic wrapper over a function preserves the contract only if it is typed by the function type itself (<F extends (...args: any[]) => any>), not by Function — and even then it does not carry over the type parameter of a wrapped generic function. The layer-by-layer breakdown is below.
Topic map
- Function types — the
(x: number) => stringexpression, the call signature, contextual typing of a callback, and thevoidleniency. - Function parameters — the optional
?, a default value=, rest...args, the ordering rule, and why?and= valueproduce different types inside the body. - Function overloads — several caller-visible signatures over one non-callable implementation, and when a union type is more honest.
- Typing this — the fake
this: Tparameter,ThisParameterType/OmitThisParameter, thenoImplicitThisflag, and why an arrow has nothisparameter. - Function wrappers — memoize, debounce, and a logging wrapper that preserve the signature via
Parameters<F>/ReturnType<F>, and the limit of the trick on a generic function.
Common Mistakes and Traps
| Mistake | Consequence | |
|---|---|---|
Putting an optional ? parameter before a required one | Compile error — optional parameters come only after required ones | |
Treating ? and = value as the same thing | With ? the in-body type is `T | undefined; with a default it is just T` |
Writing rest as args...: T or not placing it last | Syntax error — only ...args: T[], and only as the last parameter | |
| Annotating the parameters of a callback passed to a typed function | Their type is already fixed by contextual typing — an extra annotation only lets you get it wrong | |
Expecting () => void to forbid returning a value | A function returning anything fits () => void — the caller just ignores the result | |
| Thinking overloads pick a body by argument types at runtime | Overloads are compile-time; there is one body, you branch inside it, and the signatures are erased | |
| Trying to call a function via its implementation signature | Only the overload signatures are visible to callers; the implementation is hidden, even if it is wider | |
| Reaching for overloads where the result does not depend on the argument shape | Needless complexity — a union in one parameter reads clearer | |
Thinking the this parameter must be passed as an argument | It is a fake parameter — it only types the receiver and is erased on emit | |
Declaring a this parameter on an arrow function | Compile error — an arrow captures this lexically and has no receiver of its own | |
Expecting this: T to bind the receiver at runtime | It binds nothing — bind / call / an arrow still do the binding | |
Typing a wrapper as (fn: Function) => Function | The wrapped function's contract is erased: calls to the result are no longer checked | |
Returning (...args: any[]) => any from a wrapper | The same thing in other words — argument and return types are lost | |
| Expecting a wrapper to preserve a generic function's type parameter | F is pinned to a concrete instantiation — the genericity is not carried through | |
Forgetting this in a wrapper used to wrap a method | A wrapper that calls fn(...args) instead of fn.apply(this, args) loses the receiver |
Interview relevance
Functions are asked not for the syntax but to check whether you separate the contract from the implementation and compile time from runtime. Three questions do that in one stroke: "what happens to overloads after compilation", "what does this as the first parameter do", and "how do you type debounce without losing the parameters". Each probes the same thing — whether you understand that a signature is checked, not executed.
Typical checks:
- That parameters deserve annotations while the return is usually inferred — and that a callback's parameters are typed contextually.
- Parameter ordering and the difference between
?and a default value. - The function type
(x: T) => R, the call signature, and the assignment leniency for() => void. - How overloads work, why the implementation signature is not callable, and when a union type is simpler.
- What the
thisparameter is, why it is not an argument, hownoImplicitThishelps, and whatstrictBindCallApplydoes. - How to type a wrapper with
<F extends (...args: any[]) => any>andParameters<F>/ReturnType<F>— and where the trick hits its limit on a generic function.
Common wrong answer: "overloads are when the right body is picked at runtime by argument types". There is only one body, you make the choice yourself inside it, and every overload signature is gone on emit. The second classic failure is typing debounce as (fn: Function, ms: number) => Function: it compiles, but from that point on no call to the result is checked — a wrapper meant to be harmless has become a type hole the size of the whole function.