The Checker's Mind
Expert · 22 min read · ▶ live playground · ✦ checkpoint
TypeScript's checker is not guessing. It gathers evidence from your code, infers the best type facts it can, then asks one question over and over: is this value assignable, meaning is its set of possible values allowed where this target type is expected?
The four moves of inference
Inference is the checker choosing a type from evidence you wrote instead of an annotation you supplied. Most of the course has trained you to trust it; now name the four moves it uses most often.
Widening is when a precise literal fact, like "draft", becomes a broader set, like string, because mutation must stay possible. Narrowing is when a guard removes union members along one code path. Contextual typing is inference flowing from the expected position into an expression, like a callback parameter getting its type from .filter. Generic instantiation is the checker solving a type parameter such as T for one call and checking that call as if a specialized version existed.
Read that like a trace. startingStatus keeps the literal type "draft" because a const binding cannot be reassigned. editableStatus widens to string because a let binding can later hold any string. Inside labelArticle, the if narrows article.status from three possible statuses to exactly "published" on the true branch. Inside .filter and .map, contextual typing gives each article parameter the Article shape. Finally, first(articles) instantiates T as Article.
Assignability is set logic plus shape checks
You learned the master analogy in lesson 1.1: types are sets of allowed values plus safe operations. Assignability asks whether the source set fits inside the target set. For object types, TypeScript is structural: the badge scanner checks fields on the badge, not who printed it. If the required members are present with assignable types, the value fits.
A fresh object literal is an object literal checked directly at the target site; TypeScript gives it an extra typo check because it has nowhere else to have been intentionally shaped. A non-fresh variable goes through ordinary structural assignability. A const context is a request to preserve literal and readonly evidence, created by as const or a const type parameter.
savedCard has an extra internalNote, and that is fine when passed as a variable. showCard only requires title, level, and minutes. The inline break-it object is different: it is fresh, so minutez is treated like the typo it probably is. as const keeps savedCard.level as the literal "deep" instead of plain string; satisfies checks checkedCard against WorkshopCard while keeping useful precision on the expression. But satisfies is not as const: checkedCard.level stays "deep" here, while checkedCard.minutes is still number, not 20, and the properties are not made readonly.
Variance names the direction of assignability
Variance is the rule for how assignability changes when a type parameter appears inside a wrapper type. The practical question is simple: if Dog fits where Animal is expected, what happens to Producer<Dog> and Producer<Animal>?
Covariant means the wrapper moves in the same direction: if Dog is assignable to Animal, then Producer<Dog> is assignable to Producer<Animal>. Contravariant means the wrapper moves in the opposite direction. Invariant means neither direction is automatically safe because the type parameter is used both in and out. Bivariant means both directions are accepted; save the places where TypeScript permits that for 26.2.
TypeScript has variance annotations, out and in markers that document and check how a generic type parameter is meant to flow. out T means values of T come out. in T means values of T go in. in out T means both.
The Producer case is the easiest: if code only reads an Animal, a producer of Dog is enough because every dog has the animal fields. The Consumer case flips: a logger that can accept any Animal can certainly accept a Dog. The Cell case refuses the shortcut because the same wrapper both returns and accepts T.
When inference gives up
Inference needs evidence. When no value tells the checker what T should be, it often falls back to unknown, the sealed-box type that permits nothing until narrowed. When the only evidence is an empty object literal, the result can be {}, the type with no known members; as a standalone type it accepts any non-nullish value, but in a hover it usually means "the checker knows no properties here."
This is not the checker being lazy. readSlot() has no argument, no callback, and no contextual return target that solves T, so unknown is the honest answer. rememberShape({}) receives an object with no members, so {} is all it can preserve. Help inference by adding evidence: pass a value, add an annotation at the boundary, use an explicit type argument when the API truly has no input evidence, or redesign the API to return unknown and make callers narrow it.
Next lesson turns the lights on in the uncomfortable corners: places where TypeScript deliberately accepts useful JavaScript patterns even though the type proof is not perfectly sound. The same checker model still helps, but now we will demand runtime receipts for every claim.
Checkpoint
Answer all three to mark this lesson complete