skip to content

In TypeScript, what type does the compiler infer for `const n = 42` compared with `let n = 42`, and why do the two differ?

level: middleimportance: must knowfreq 66%

answer

  1. depends on the declaration, not the value
  2. can it be reassigned later?
  3. widening to the primitive type
  4. annotation overrides the inference
  5. identical emitted JavaScript either way

basics

~20 s

TypeScript infers the narrow type 42 for const n = 42 and the wider type number for let n = 42. A const can never be reassigned, so the narrow type stays accurate; a let can be, so the compiler widens it to the primitive.

solid answer

~40 s

Inference for a primitive initializer depends on how the variable is declared. `const n = 42` can never hold anything else, so the compiler keeps the precise type `42`. `let n = 42` may be reassigned later, so keeping `42` would make almost every future assignment an error — the compiler *widens* the inferred type to `number` instead. The same happens with `const s = "ready"` versus `let s = "ready"`, and with `true`. An explicit annotation always overrides inference in both directions: `const n: number = 42` deliberately widens, and `let n: 42 = 42` deliberately keeps it narrow at the cost of allowing only that one value. None of this affects emit — the annotation and the inferred type are both erased, and the JavaScript is identical either way.

code

typescript · 10 lines
typescript
const exact = 42;   // type: 42
let   loose = 42;   // type: number

loose = 43;         // ok

const wide: number = 42; // annotation widens on purpose
let   pinned: 42 = 42;   // annotation keeps it narrow

// @ts-expect-error 43 is not assignable to type 42
pinned = 43;

go deeper

for a junior

Recall the pair: a const initialised with 42 gets the exact type, a let gets number. Say plainly that it depends on whether the variable can be reassigned.

for a middle

Explain the mechanism: keeping the precise type is only sound when the binding cannot change, so mutable declarations are widened to the primitive. Show that an explicit annotation overrides inference in either direction.

for a senior

Demonstrate the practical consequence — where precise inferred types get carried into function calls and property initialisation, and why a mutable variable that will later hold something else needs its annotation written up front rather than inferred from its first value.

for a principal

Frame it as an information-preservation choice across a codebase: default to const so the checker keeps the sharpest facts, and treat a widely-shared mutable module-level value as a design smell that costs both type precision and reasoning ability.

## What the checker infers When you declare a variable with an initializer and no annotation, TypeScript infers a type from the initializer. For primitives the answer depends on the *declaration form*: ```ts const a = 42; // a: 42 let b = 42; // b: number const c = "ready"; // c: "ready" let d = "ready"; // d: string const e = true; // e: true let f = true; // f: boolean ``` Hover any of these in an editor and that is what you see. Nothing about the *value* differs between the pairs; only the declaration keyword does. ## Why mutability decides it A type is a promise about every value the variable can ever hold. `const` binds a primitive once and forever, so the most precise possible description of it — the exact value — is also a correct one, and precision is free. There is no future assignment that could violate it. `let` has no such guarantee. If the compiler inferred `42` for `let b = 42`, then `b = 43` would be an error, and the declaration would be almost useless. So the compiler applies **widening**: it takes the precise inferred type and replaces it with the general primitive type it belongs to — `number`, `string`, `boolean`, `bigint`, `symbol` — which is what you almost always meant when you wrote a mutable variable. The same widening happens anywhere the value can be reassigned or mutated later, which is why a mutable variable initialised from a `const` still ends up wide: ```ts const a = 42; // 42 let g = a; // number — g is mutable, so the narrow type is widened again ``` ## An annotation always wins Inference only runs when you have not said what you want. An explicit annotation overrides it in either direction: ```ts const wide: number = 42; // number, on purpose let narrow: 42 = 42; // stays 42; narrow = 43 is an error ``` That is the practical control you have. If you need a `const` to be treated as the general primitive — say, because you will pass it somewhere that infers from it and you do not want the precision to propagate — annotate it. If you need a mutable variable pinned to a specific value, annotate that too, and accept that every reassignment must match. ## Where the difference actually bites Most of the time you never notice, because the narrow type is a *subtype* of the primitive: a value of type `42` goes anywhere a `number` is accepted, and `"ready"` goes anywhere a `string` is accepted. The precision is a bonus, not a restriction. It starts to matter when the type is *carried somewhere* — passed to a function that infers from its argument, used to initialise a property, or compared against a fixed set of allowed values. There, a `const` gives the checker exact information and a `let` hands it only "some string". That is the reason the reflex "declare with `const` unless you truly reassign" pays off in TypeScript beyond its usual JavaScript benefits: you keep information the checker can use. The reverse case is worth knowing too. A mutable variable that starts at one value and later holds something else needs an annotation up front, because the initializer alone cannot describe the whole story: ```ts let count = 0; // number — fine, later counts are numbers too let total: number | null = null; // annotate: the initializer alone says only 'null' ``` ## None of it exists at runtime Both the inferred type and the annotation are erased. `const a = 42` and `const a: number = 42` emit the identical JavaScript, and there is no runtime cost or check attached to either. The difference is entirely about what the checker will let you write next — which is the whole point of the type layer, and also why the distinction is invisible until an error message tells you about it. ## The short version `const` keeps the precise inferred type because it cannot change; `let` widens to the primitive because it can. An explicit annotation overrides whichever default you did not want, and nothing about the choice reaches the emitted JavaScript.

  • How do you make a `const` behave as though it were declared with `let` for inference purposes?
    Annotate it: `const n: number = 42` gives the widened type explicitly. That is the deliberate way to say "treat this as a number, not as that exact value" — useful when the precision would otherwise propagate somewhere you did not want it. Inference only runs when you have not stated a type, so an annotation always wins.
  • Why does `let g = a` widen even when `a` is a `const`?
    Because widening is a property of the *destination* declaration, not the source. `g` is mutable, so the checker cannot promise it will only ever hold the value `a` currently has, and it generalises to the primitive type. If you want `g` to keep the narrow type you have to annotate it, and then accept that every later assignment must match.
  • Why is `let total = null` usually a mistake in a strict codebase?
    The initializer is the only evidence the checker has, and it says the value is `null`. Whatever you assign later will not fit. Annotate the full range up front — `let total: number | null = null` — so the declaration describes the variable's whole life rather than just its first instant.

saying these in an interview costs you the question

  • Says const and let infer the same type from the same value.
  • Thinks const makes the variable's contents immutable at the type level.
  • Believes widening happens at runtime.
  • Claims you cannot get number from a const without an assertion.
  • Assumes the narrow type prevents passing the value where a primitive is expected.

context