In TypeScript, the function `function add(a, b) { return a + b; }` is rejected under the `noImplicitAny` compiler option, yet the same function needs no return type annotation at all. Why does the compiler treat parameters and return types so differently?
answer
- evidence the compiler can actually see
- body is present, callers are not
- no usage-based inference for inputs
- fallback type when nothing is known
- the flag inside strict that reports it
basics
~20 sTypeScript infers a return type from the function body, but nothing tells it what callers will pass, so unannotated parameters fall back to the implicit any type, which noImplicitAny rejects. Annotate the inputs; let the body infer the output.
solid answer
~50 sInference needs evidence, and the two positions have very different amounts of it. The body of `add` is right there in the source, so once the parameters have types the compiler can compute the type of `a + b` and use it as the return type. Parameters are the opposite: they are the function's inputs, and a plain function declaration contains nothing that says what callers will hand it. Each unannotated parameter therefore falls back to an implicit `any`, and `noImplicitAny` — which `strict` turns on — reports that silent hole as an error. The practical rule follows directly: annotate parameters, and let the return type be inferred unless you have a specific reason to pin it. The one place parameters get types for free is a function expression written where an expected type already exists, such as a callback argument, because the surrounding context supplies them.
code
typescript · 6 linesfunction add(a: number, b: number) {
return a + b;
}
const total: number = add(1, 2);
console.log(total);go deeper
Be able to say plainly that you annotate parameters and usually let the return type be inferred, and that an unannotated parameter is an implicit any that noImplicitAny flags.
Explain the mechanism: the body is available evidence for the return type, the future call sites are not, and TypeScript deliberately skips usage-based inference for parameters.
Show what an implicit any costs in a real codebase — the any flows into the return type and out through every call, quietly disabling checks far from the original function.
Own the policy angle: whether noImplicitAny is enforced repo-wide from day one or ratcheted in during a migration, and how you stop new implicit anys from entering while the old ones are still being paid down.
## The one rule behind the asymmetry TypeScript infers what it can derive from the source in front of it and refuses to guess at anything else. A function body is source it can read; the set of future call sites is not. That single fact explains the whole asymmetry between parameters and return types. ## Why the return type comes for free Give the parameters types and the body becomes fully checkable: ```ts function add(a: number, b: number) { return a + b; } ``` The compiler types `a + b` as `number`, collects the types of every `return` expression, unions them, and uses the result as the function's return type. `add` has type `(a: number, b: number) => number` even though nobody wrote `: number`. With several returns it unions them: ```ts function find(id: string, table: Record<string, string>) { if (id in table) return table[id]; // string return null; // null } // inferred: (id: string, table: Record<string, string>) => string | null ``` If a function never returns a value, the inferred return type is `void`; if every path throws or loops forever, it can be `never`. ## Why parameters cannot be inferred There is no equivalent evidence for an input. A function declaration is written once and called from anywhere — possibly from files that do not exist yet, possibly from a different package. Inferring `a` from the call sites would mean the meaning of a function depended on who happened to call it, and adding a new caller could retroactively change the type of an existing one. TypeScript does not do that. It also, importantly, does **not** infer parameter types from *usage inside the body*. Writing `a.toUpperCase()` does not make `a` a `string`: ```ts function shout(a) { // error under noImplicitAny: 'a' implicitly has an 'any' type return a.toUpperCase(); } ``` Some other languages do this kind of usage-based inference; TypeScript deliberately does not, because it would make the parameter type depend on the implementation rather than on the declared contract. ## What implicit `any` actually costs With `noImplicitAny` off, the code above compiles, `a` is `any`, and the damage spreads. `a.toUpperCase()` is `any`, so the inferred return type is `any`, so every call expression is `any`, so every downstream assignment from that call is unchecked. One un-annotated parameter can silently disable checking across a whole call chain. That is why `noImplicitAny` exists and why it is part of the `strict` family: it turns a silent hole into a visible one at the exact place the information is missing. Note the flag's precise name and scope — it complains about *implicit* `any`. An explicit `a: any` is still legal; the flag only objects when the compiler had to fall back to `any` on its own. ## Where parameters do get types without annotations There is one genuine exception, and it is not a hole in the rule — it is the same rule applied in the other direction. When a function *expression* is written in a position that already has an expected type, the expected type supplies the parameter types: ```ts const lengths = ["a", "bb"].map((s) => s.length); // s: string, no annotation needed ``` Here the evidence exists in the source: the declared signature of the callback parameter. That mechanism is contextual typing. It works for function expressions and arrow functions, never for a standalone function declaration, which by construction has no surrounding expected type. ## The habit to take away Annotate parameters; leave the return type inferred by default. The parameter annotations are the ones carrying real information — they are the function's contract with its callers and the compiler cannot recover them. The return annotation mostly restates what the body already proves, which is why it is optional and why it is worth adding only for specific reasons, such as pinning a public contract or breaking the inference cycle in a recursive function. ## Erasure, as always None of this survives compilation. `a: number` is checked at build time and stripped from the emitted JavaScript; no argument is validated at runtime. Annotating a parameter tells the compiler what you promise callers will pass, not what the function will verify.
- With noImplicitAny turned off, what return type does the compiler give that function?`any`. With `a` and `b` both implicitly `any`, `a + b` is `any`, so the inferred return type is `any` as well. Every call expression then produces `any`, and assignments made from it stop being checked. That silent propagation down the call chain is precisely the failure `noImplicitAny` is there to surface.
- Does TypeScript ever infer a parameter's type from how the parameter is used in the body?No. Calling `a.toUpperCase()` inside the function does not make `a` a `string`; it stays implicitly `any`, or errors under `noImplicitAny`. Parameter types come from an annotation, from a default value's type, or from a contextual type at the position where a function expression is written — never from the implementation's usage of the parameter.
- Are class methods and object-literal methods treated the same way?A class method behaves like a declaration: its parameters need annotations. An object literal's method can be typed contextually — if the literal is checked against a type that declares that method, the parameters take their types from that declaration, the same mechanism that types callbacks.
saying these in an interview costs you the question
- Says TypeScript infers parameter types from how they are used in the body
- Thinks an unannotated parameter defaults to unknown rather than any
- Believes annotations check arguments at runtime
- Claims noImplicitAny must be enabled separately when strict is already on
- Annotates every return type but leaves parameters bare