A TypeScript codebase annotates every callback parameter with the built-in `Function` type. What checking does that give up, and what should replace it?
answer
- callable, and nothing more
- no arity, no parameter types
- the call result is any
- say the signature you mean
- never in, unknown out
basics
~20 sFunction says only that a value is callable: any argument list type-checks and every call produces any. Replace it with an explicit call signature giving parameter and return types, or with a constraint such as (...args: never[]) => unknown where the function is never actually called.
solid answer
~50 s`Function` is the standard library's interface for "some callable thing" — it carries `call`, `apply`, `bind`, `length` and `name`, and nothing about the signature. So a parameter typed `Function` accepts any function *and* any class, can be invoked with any arguments in any order, and the call expression has type `any`. That `any` then spreads: whatever you do with the result is unchecked, and the bug lands somewhere else entirely. Replace it with a real call signature — `(item: Item, index: number) => boolean` — which is the whole point of the type layer. Where the function is genuinely opaque, say so precisely: `(...args: never[]) => unknown` accepts any function while making an unchecked call impossible, and for a wrapper that forwards arguments, capture them generically as `<A extends unknown[], R>(fn: (...args: A) => R)`. Prefer either over `(...args: any[]) => any`, which reintroduces the leak.
code
typescript · 17 lines// Unchecked: any arguments accepted, result is any.
function invoke(fn: Function): void {
fn(1, 'two', true);
}
// Checked: arity, argument types and return type all enforced.
function invokeTyped(fn: (n: number) => string): void {
const out: string = fn(1);
console.log(out);
}
// Opaque but safe: any function accepted, calling it is impossible.
type AnyFunction = (...args: never[]) => unknown;
function register(name: string, handler: AnyFunction): void {
console.log(name, typeof handler);
}
register('parse', (input: string) => input.trim());go deeper
Know that Function only means "this is callable" and that writing the actual parameter and return types instead is what makes a callback checked.
Explain the concrete losses — arity, argument types, and a result typed any — and write the replacement call signature for a given callback.
Grade the replacement by intent: exact signature when known, (...args: never[]) => unknown for a function you only store, a generic rest-tuple capture for a wrapper. Be ready to describe how you would migrate an existing codebase without hiding the errors you uncover.
Own the policy: which escape hatches remain legal, how any-returning calls are prevented from spreading across module boundaries, and how the cost of a large migration is staged against the incidents it prevents.
## What `Function` actually is `Function` is a real interface in the standard library, describing the members every function object has: `apply`, `call`, `bind`, `toString`, `length`, `name`, `prototype`. Notice what is *not* there — a call signature describing this particular function's parameters and return type. `Function` is the type of "a function object", not the type of "a function that takes an `Item` and returns a `boolean`". The compiler nevertheless lets you invoke a value typed `Function`, with any arguments at all, and types the result `any`: ```ts function invoke(fn: Function): void { fn(1, 'two', true, {}); // no arity check, no type check const result = fn(); // result: any } ``` ## The three things you lose **Arity.** Passing three arguments to a one-parameter callback compiles. Passing none to a callback that requires two compiles. The most common real-world callback bug — arguments in the wrong order, because a `map` callback is `(value, index)` and someone wrote `(index, value)` — is completely invisible. **Argument and return types.** Nothing relates what you pass to what the callee expects, and the result is `any`. That is the serious one: `any` is not merely "unknown", it is *assignable to everything*, so the unchecked value flows onward and the eventual failure is far from the annotation that caused it. **Kind.** A class is a function object, so a class is assignable to `Function`. The type cannot distinguish a plain callback from a constructor, an async function, or a generator function. ## What to write instead The default answer is simply the signature you mean: ```ts function filterItems(items: Item[], predicate: (item: Item, index: number) => boolean): Item[] { return items.filter(predicate); } ``` Now a wrongly-ordered or wrongly-typed callback is an error at the call site, which is where the mistake was made. When the function is genuinely opaque — you store it, count it, compare it, but never invoke it — say that precisely: ```ts type AnyFunction = (...args: never[]) => unknown; function register(name: string, handler: AnyFunction): void { /* stored, not called */ } register('a', (n: number) => n + 1); // accepted register('b', async (s: string) => s); // accepted ``` Parameters are compared contravariantly, so `never` in the parameter position accepts a function with any parameter types; and because the parameters are `never`, you cannot then call `handler` with anything, which is exactly the discipline you want. The return type `unknown` forces a narrowing step before the result is used, instead of silently becoming assignable to everything. When you *do* need to call it — a wrapper, a memoiser, a timing decorator — capture the shape generically: ```ts function timed<A extends unknown[], R>(fn: (...args: A) => R): (...args: A) => R { return (...args: A): R => { const start = Date.now(); try { return fn(...args); } finally { console.log(Date.now() - start); } }; } ``` The wrapper preserves the original signature end to end, so callers of the wrapped function get the same checking they had before. ## Why `(...args: any[]) => any` is not the fix It is better than `Function` in one respect — it does describe a function rather than a function object — but it hands back `any` on every call and accepts any arguments, so both leaks remain. It is a reasonable last resort inside a declaration you cannot change; it is not a type you should be writing in new application code. ## Migration judgment In an existing codebase, replacing `Function` everywhere at once produces a wall of errors and a temptation to paper over them with assertions. The productive order is: fix the ones whose result is used (those are the `any` leaks that cause incidents), then the ones that are invoked, and finally the store-and-forward ones, which are usually a mechanical swap to the opaque form. Each conversion is a genuine bug hunt, not a formality — mismatched arity is common and has been compiling silently the whole time. ## Interview framing Lead with the mechanism — "`Function` says callable and nothing else, so calls are unchecked and return `any`" — then give the replacements graded by intent: exact signature when you know it, `(...args: never[]) => unknown` when you never call it, a generic capture when you wrap it. That progression is what separates a memorised "don't use `Function`" from real judgment.
- What does calling a value typed `Function` return, and why is that worse than a compile error?It returns `any`. A compile error stops you at the mistake; `any` is assignable to everything, so the unchecked value flows into the rest of the program and the failure appears somewhere unrelated — a property read on `undefined`, a string where a number was expected. The annotation has effectively disabled checking for everything downstream of that call.
- You need a generic that accepts any function so you can wrap it and forward its arguments — what do you write?Capture the shape as type parameters: `function wrap<A extends unknown[], R>(fn: (...args: A) => R): (...args: A) => R`. The rest parameter tuple `A` and the return type `R` are inferred from the argument, so the wrapper's own signature is identical to the wrapped function's and callers keep full checking.
- Does `Function` include class constructors?Yes — a class declaration produces a function object at runtime, so a class is assignable to `Function`. That is part of the problem: the type cannot distinguish a plain callback from a constructor, an async function or a generator, so a value that must not be invoked directly slips through unnoticed.
saying these in an interview costs you the question
- Thinks `Function` constrains parameter count or return type
- Believes calling a `Function`-typed value is a compile error
- Says `(...args: any[]) => any` is the type-safe replacement
- Confuses the `Function` type with the `function` keyword
- Argues it is fine because runtime behaviour is unchanged anyway