Skip to content
AtomicReps
Types That Earn Their Keep

Reading the type system

Lesson 1 of 27

Reading the type system

A lesson from Types That Earn Their Keep. Play it above, or read it through below.

  • Inference reads what you handed it, and where there is nothing it writes any

    The common read: TypeScript works out a type from how a value is used, so leaving the annotation off costs nothing and the checker fills in whatever I meant.

    Inference does not search for what you meant. A variable's type comes from its initializer, an array's element type comes from a best common type chosen out of the candidates actually present, and where those candidates share a base class that nothing in the array actually is, the handbook's own worked example says the compiler makes no inference about the element type and hands back the union instead.

    Nothing flows backwards from a later line. Where there is no value to read at all the answer is not an error and not a guess, it is any.

    What turns that fallback from silence into a diagnostic is a flag rather than a language rule, noImplicitAny, which you get for free in 7.0.2 because strict defaults on, and it fires where the compiler can see that IT gave up (TS7006 on an unannotated parameter, TS7008 on a member of an object type with no type of its own) and it has nothing to say about an any that arrived from a library signature or from your own keyboard.

    That gap is why a review comment reads "this file is fully typed, strict is on, we are covered", on a configure(opts: { retries: number, onError }) that accepts literally anything in its second field, filed as a callback bug in the consumer three sprints later when someone passes an object. Annotate the boundaries and let inference have everything else; the annotation is not documentation of what the value is, it is the only way to say something the values in front of the compiler do not already say.

  • The types are a second program, and it never runs

    The common read: The types are part of the program the machine runs, so a shape written in type position is still being enforced while the code executes.

    How much of what you write in type position survives the build?

    Point tsc at a file that annotates everything - a type alias, typed parameters, a typed return. What crosses into the emitted JavaScript is

    tsc deletes the annotations and emits the JavaScript underneath, and the handbook states the consequence in one sentence: "Type annotations never change the runtime behavior of your program". What you are reading in a .ts file is two programs sharing one set of characters: one of them runs, and the other one is a proof obligation discharged once, at build time, by a checker that has no access to any value your users will ever send.

    The second program is also opt-in: the type of a variable is inferred from its initializer and most positions never need one.

    This is why a ticket reads "the Cents type is not taking effect in production, the build must be serving an old bundle" and gets filed against the pipeline, by someone who searched the deployed file for the word Cents, found nothing, and drew the only conclusion their previous runtime allowed. If you want a check at the boundary, you write one and you pay for it in runtime code; the annotation buys you exactly one thing, which is that the rest of the file is checked against it.

  • any is not a type, it is the checker leaving the room

    The common read: strict is on, so anything the compiler did not flag has been checked, and an as is how I tell it something it could not work out for itself.

    A clean build is not evidence that anything was checked. any is not a permissive type that accepts a lot of values, it is an instruction to stop asking: the disabling is contagious, because every property you read off an any is itself any and travels into the next function with the checking still switched off.

    An assertion is the same hole with better manners. as is a code review where you are the only reviewer. It is erased exactly like an annotation, "there is no runtime checking associated with a type assertion", and "there won't be an exception or null generated if the type assertion is wrong", so the only thing as User changes is what the compiler will say about the lines after it.

    There is one guard rail and it is thinner than it looks: assertions are only permitted between types that are more or less specific versions of each other, which rejects "hello" as number and rejects nothing at all when the source is any, and the documented way through the guard rail when it does bite is two assertions, expr as any as T.

    This is how a review approves const data = response as UserProfile as a validation step, and how the resulting ticket gets filed against whoever wrote the consumer, three modules away, with a stack that never mentions the line where the lie was told. Put an assertion where you would have been willing to write a comment saying "trust me", and nowhere else, because the compiler will believe you completely and permanently and it will never mention it again.

  • unknown is the same top with the questions left on, and never is the far end

    The common read: unknown is any with a nag attached and never is a fancy way of writing void, so reaching for any costs me a lint complaint at worst.

    Swap an any at a boundary for an unknown. Which values does that turn away?

    None. unknown holds exactly the same values any holds and grants exactly none of the same permissions. The handbook puts the two side by side: unknown "represents any value. This is similar to the any type, but is safer because it's not legal to do anything with an unknown value", which means no property read, no call, no arithmetic, until you have proved what it is, and 7.0.2 reports all three as TS18046 on the line that tried.

    never is the type no value inhabits, and the reference states the pair as inverses in as many words: "Everything is assignable to unknown, never is assignable to everything. Nothing is assignable to never, unknown is not assignable to anything (except any)." any is not the top of this lattice, it is a hole punched through it, assignable both ways to everything and refused by exactly one type, never.

    The pattern JSON.parse(raw) as Payload keeps arriving in postmortems, escalated against the upstream service for sending a bad payload and closed with a schema change nobody upstream needed, while const raw: unknown = JSON.parse(...) never gets there: same values, same ignorance about them, and one of the two makes you write the check down before the consumer can be written at all. Reach for unknown at every boundary you do not control and let never be the thing that tells you a branch is impossible.

  • A name is resolved in a space, and there are two of them

    The common read: A type name and a value name are the same name, so String and string are one thing spelled two ways and typeof x in a type is the operator I already know.

    The same characters mean different things depending on which side of the file's grammar you wrote them on. The handbook has no single name for this and states it as a reminder instead, "Remember that values and types aren't the same thing". const Config = {...} and type Config = typeof Config can sit in one file with no collision, which is legal at 7.0.2 and is the idiom everything in this language's configuration code is built on.

    String, Number and Boolean are legal types referring to different built-ins, the handbook's instruction is "Always use string, number, or boolean for types", and the wrapper declares every method the primitive has, so the mistake survives every use and dies at the first boundary that wants the primitive back.

    The same trio problem repeats one level up with object, which "refers to any value that isn't a primitive", against the global Object, which admits primitives too, against the empty object type {}, which admits everything except null and undefined, and the handbook's own summary of the three is "object is not Object. Always use object!".

    typeof is the bridge from a value back to its type, and it is deliberately not the operator you already know: it is legal only on identifiers or their properties, so typeof makeUser() is not a narrower feature but a syntax error, and what you wanted has a name in another lesson, ReturnType<typeof makeUser>. Read the position before you read the name. When a review comment says "why is this capitalized", treat it as a bug report rather than as a style note.