A value typed `any` in TypeScript is often called contagious. What exactly happens to the types of expressions derived from it, and which checks does the compiler stop performing?
answer
- it does not stay in one place
- derived expressions inherit the type
- assignment keeps the declared annotation
- typeof and comparisons escape it
- runtime error, no compile error
basics
~20 sA value typed any makes every expression derived from it any as well — property reads, calls, indexing, awaits — so whole branches of code stop being checked, and the failure surfaces at runtime far from the original annotation.
solid answer
~50 s`any` does not stay where you put it. Read a property off an `any`, call it, index it, or `await` it, and the result is `any` again, so the infection spreads along the whole chain of derived expressions and nothing in that chain is checked — misspelled properties, wrong argument counts and impossible arithmetic all pass. It also flows outward through assignment: `const n: number = someAny` compiles, and afterwards `n` is statically a `number` even if it holds a string at runtime, so the annotation is believed rather than verified. Type arguments inherit it too — an `any` inferred for a generic makes the whole resulting structure permissive. A few things escape: `typeof x` is still `string`, comparisons still produce `boolean`. The practical consequence is that the error appears at runtime, far from the `any` that caused it.
code
typescript · 9 linesdeclare function loadConfig(): any;
const config = loadConfig();
const retries = config.retry.count; // any - no check on retry or count
const doubled = retries * 2; // any - arithmetic unchecked
const label: string = config.name; // allowed; label is string regardless
const kind = typeof config; // string - typeof is not infected
const same = config === retries; // boolean - comparison is not infectedgo deeper
Know that using an any-typed value gives you back more any, so the compiler stops warning you about anything in that chain. Be able to say a typo in a property name would go unreported.
Explain the propagation rule concretely — property access, calls, indexing and await all yield any — and name the operators that keep their own result type.
Demonstrate the diagnosis: trace backwards to the first expression the editor types as any, and explain why assignment into a typed variable is the most dangerous step rather than the safest.
Own the containment strategy: where any is permitted, how boundaries are wrapped so an untyped dependency cannot silently unchecked a subsystem, and what backstop catches leaks at scale.
## What "contagious" actually means The metaphor is precise, not loose. `any` is not a value-level property that stays in one variable; it is a compile-time state that the checker propagates through expressions. Every time it computes the type of an expression whose operand is `any`, the honest answer is "I have no idea" — and the type it records for that expression is `any` again. ## The derived-expression rule For a value `x` typed `any`, all of these are typed `any`: ```ts declare const x: any; x.foo; // any x.foo.bar(); // any x[0]; // any x(); // any await x; // any x + 1; // any ``` None of them is an error. The compiler will not tell you that `foo` does not exist, that `x` is not callable, that you passed three arguments to a two-parameter function, or that adding a number to it makes no sense. Every guarantee the type system normally gives you is suspended for the entire chain, and the chain can be long: one `any` at the top of a data-processing pipeline leaves every downstream step unchecked. ## What does not get infected A handful of operators have a result type fixed by the language regardless of operand type, and those stay honest: ```ts const kind = typeof x; // string const eq = x === 1; // boolean const neg = !x; // boolean ``` This matters in practice, because it is exactly these operators that narrowing is built on — which is why a value you deliberately retype as `unknown` can still be inspected before use. ## Assignment: the quiet one The subtler half of the propagation is not that `any` spreads, but that it **satisfies**. `any` is assignable to every type except `never`, so: ```ts declare const parsed: any; const n: number = parsed; // no error n.toFixed(2); // no error; throws if parsed was a string ``` After the assignment, `n` is statically a `number`, not an `any` — the contagion stops there, and that is precisely what makes it dangerous. From this point on the compiler is confident, the editor autocompletes number methods, and the value is whatever the wire actually sent. The annotation looks like a safety measure and functions as a lie, because nothing verified it. Remember that types are erased: there is no runtime check inserted at an annotation, ever. ## Generics and structures `any` also travels through type arguments. If a generic function's parameter is inferred from an `any` argument, the type parameter is inferred as `any`, and the returned structure carries it: ```ts declare function firstOf<T>(items: T[]): T; declare const rows: any; const row = firstOf(rows); // T is any, row is any ``` Similarly, `any[]` yields `any` elements, `Promise<any>` yields `any` when awaited, and a `Record<string, any>` yields `any` for every lookup. This is how a single untyped dependency ends up making a whole module effectively unchecked while every file in it still *looks* fully annotated. ## Why the error appears far from the cause The defining symptom of an `any` bug is distance. The `any` is introduced at a boundary; the wrong property name is written three modules away; the crash happens in a rendering path at 3am. Because no compile error was ever produced, git blame points at innocent code. When you debug this class of failure, walk backwards to the first expression whose type the editor reports as `any` — hovering values in the editor is the fastest diagnostic, since the type the tooling shows you is the compiler's actual answer. ## Containment The fix is not heroic: stop the `any` at the door. Annotate the boundary result as `unknown` so the compiler refuses to let it flow, then validate it into a real type once, in one place. For the cases you cannot annotate — an untyped dependency, a deliberately dynamic helper — keep the `any` inside a single small function whose signature is honest, so callers see the real type and the unchecked region is a few lines rather than a subsystem. Type-aware lint rules exist that flag values of type `any` being assigned, called or passed, which is a useful backstop when a codebase is large enough that review will not catch every leak.
- If `any` is this dangerous, why does the compiler allow assigning it to a typed variable at all?Because TypeScript is a gradual type system layered onto existing JavaScript. Its adoption story depends on untyped code being able to call typed code and vice versa without ceremony, and `any` is the seam that makes that possible. The cost is soundness: the assignability rule that makes migration painless is the same rule that lets a wrong value acquire a confident type.
- Does replacing an `any` annotation with `unknown` change the emitted JavaScript?No. Type annotations are erased during compilation, so both forms emit the identical JavaScript and cost nothing at runtime. The only difference is which programs the compiler accepts: with `unknown`, the code that reads members off the value stops compiling until you add a check, so the change is caught at build time rather than paid for in production.
- How do you find where an `any` entered a chain of code?Work backwards from the surprise. Hover the expressions along the chain in the editor and find the first one the compiler reports as `any` — that is the entry point, not the line that crashed. Common sources are `JSON.parse`, a response body, an untyped import, and an explicit `as any` someone added to unblock a build.
saying these in an interview costs you the question
- Thinks the any stops at the variable it was declared on
- Assumes a type annotation re-checks the value at runtime
- Says any and unknown behave the same for derived expressions
- Blames the runtime error on the line where it throws