Finale: thinking in types
Eleven chapters have covered the syntax. This one is about how to think. TypeScript is JavaScript plus a layer of types that exists only while you write and compile: the compiler checks the types, then removes them, and the JavaScript that runs is the JavaScript you wrote. Five ideas, one map of the course, and a final quiz.
Idea 1 — three forms: annotation, as, and satisfies
The same config object, written three ways. The difference is what gets checked and what gets inferred.
Three ways to tell the compiler what a value is
Giving a value a type is a message to the compiler, and these three forms send three different messages. An annotation (: Config) means "check this": the compiler checks the value, then replaces the inferred type with the type you declared. An assertion (as Config) means "take my word for it": the check is skipped. satisfies (added in TypeScript 4.9) means "check it, but do not rewrite it": the shape is checked and the inferred type is left alone. Compare them below.
An object literal assigned straight to a typed variable gets the excess property check: Object literal may only specify known properties, but 'thema' does not exist in type 'Config'. Did you mean to write 'theme'? (TS2561). That check is from chapter 04.
One more keyword often appears next to these three: as const. It does not check a shape. It does something else: it stops widening. Literal values keep their exact literal types, and every property becomes readonly.
satisfies checks the shape, and as const keeps the literals. They can be used together: {…} as const satisfies Config.Annotating everything is a beginner habit
Inference already does most of the work. Writing const total: number = 22 adds nothing the compiler did not already know, and it makes the code longer and harder to change.
Two places are worth annotating. Function parameters, because a parameter comes from outside and inference has nothing to read. Public boundaries — exported functions, module APIs, shared data — because there the type is a promise to other code, and writing it down means the compiler checks the promise instead of copying whatever you happened to return. Everywhere else, let inference do it.
Idea 2 — what an assertion costs
Every as says: I know more about this value than the compiler does. Sometimes that is true. The question is where your information comes from.
as is not a bad keyword. It exists for the cases where you really do know more than the compiler. There is one test: where does your information come from? If it comes from something you checked yourself, the assertion is reasonable. If it comes from not wanting to handle the other case, it is not.
querySelector returns Element | null. The compiler has not read your HTML and you have. That gap in information is what makes the assertion reasonable. It is still a promise you are making, so if the markup changes, this line becomes wrong silently.localStorage, form input: the shape of that data is not controlled by your codebase. Here as is not knowledge, it is a guess written down as a fact. Section 03 shows what to do instead.as unknown as T: the loudest signal in the type system
When two types have nothing in common, a direct as is rejected, so people write x as unknown as T to get past it in two steps. Read what that actually means: "discard everything the compiler knows about this value, then let me relabel it". In test code it is sometimes a reasonable escape hatch. In application code it usually means the types are modelled wrongly, and the model is what should change.
TypeScript is deliberately not fully sound
A type system is sound when a program that passes the check cannot fail in a way the types said was impossible. TypeScript is not sound, and this is a design decision, not a bug. A fully sound system would reject a great deal of ordinary JavaScript, so TypeScript trades some guarantees for being usable on real code.
It helps to know the specific holes, because these are the places where a green compile still means nothing.
any and as rare, check data at the boundary yourself, and turn on noUncheckedIndexedAccess when the project can take it.Idea 3 — unknown at the boundary
The compiler's authority ends when compilation ends. Data that arrives while the program runs has to be checked by code you wrote.
Types are erased during compilation. That has one cold consequence: every interface you wrote is gone in production, and data arriving from outside is not validated by anything. An interface that describes an API response is a claim about the data, not a guarantee. Step through what that looks like.
Order as far as the compiler is concerned.A type error is not a runtime error
Beginners assume a type error stops everything. It does not. By default tsc reports the error and still writes the JavaScript file. Set noEmitOnError if you want it to stop. Many build tools go further and remove types without checking them at all, so the type error never even appears during the build.
So a red underline is a message, not a wall. It is worth saying plainly, because it explains something people find confusing: code with type errors can still run, and can still be shipped. Making the check part of continuous integration is what turns the message into a wall.
The isOrder function standing at the boundary is the type predicate from chapter 03: a check that runs on the value, in exchange for narrowing at the type level.
as on line 10. It lives inside the function, it exists only so the fields can be read one by one, and every field is then actually checked. That is an assertion helping a check, not an assertion replacing one.Writing every validator by hand gets tiring
In a real project the shapes get large, and a hand-written validator gets long and easy to get wrong. Runtime validation libraries such as zod solve this: you describe the shape once, and both the validation function and the TypeScript type are generated from that one description.
This course does not cover them. One sentence is enough: they exist because types are erased, so something has to do the checking at runtime. The idea is the same as the isOrder you just read.
Idea 4 — any is a tool, not a habit
Criticising any is easy. Using it well is harder. The rule is about how far it can spread.
Eleven chapters have said "avoid any". Here is the fair version. any has legitimate uses: old code during a migration (the transitional state from chapter 10), and a genuinely dynamic boundary, such as the result of eval or a third-party callback whose shape is not documented anywhere. The discipline is one line: keep it in the smallest scope that works.
any spreads: anything read out of an any value is any as well. Once it reaches an exported signature, every caller in the project loses its checks too.Reach for unknown first, and for any only if you must
Both accept any value. The difference is what you may then do with it. unknown must be narrowed before use, so the compiler forces you to add the check. any allows every use, so no check is required and none is performed.
So when you do not know the type, start with unknown. It is the type-safe way of saying "I do not know yet". Drop to any only when unknown genuinely makes the code impossible to write, and keep it in the smallest scope you can.
Idea 5 — types as documentation: make illegal states unrepresentable
A comment can go out of date and a document can go unread. A type cannot be quietly ignored, because code that contradicts it does not compile.
The same order status, modelled two ways. On the left every field is optional, so impossible combinations can be built freely. On the right the discriminated union from chapter 03 writes down which fields exist in which state.
switch (order.status) each branch has exactly the fields that state has, and never catches a state you forgot. Chapter 03 gave you the technique. This chapter gives the reason to use it: put the agreement in the type instead of in a comment beside it.Why this idea comes last
The first four ideas are about getting along with the compiler. This one is about what the type system is actually for. It is not a spell checker. It is a language for describing the rules of your problem. "An order cannot be delivered before it is paid" is such a rule. Written into the type, every future change that breaks it fails to compile.
Notice what the union discriminates on: status, a real property that exists at runtime. It cannot discriminate on the name of a type, because TypeScript compares the shape of a type, not its name, and the name is gone after compilation. Chapter 04 covers that comparison in detail.
Building your own type helpers
The tools from chapter 06 and the parts from chapter 07, put together. Build two of them yourself.
First one: MyOmit, a rewrite of the Omit you have been using since chapter 06. All the parts come from chapter 07. A mapped type walks the keys, and as key remapping (TypeScript 4.1) removes the ones you do not want.
never does the work again here. A key mapped to never is removed from the result, which makes never a general way to delete things in type-level code.Second one, harder: DeepReadonly. The built-in Readonly only locks the top level, so a nested object can still be changed. To lock all of it, let the mapped type call itself.
object too, and there is no point recursing into it. That is a medium-level problem on type-challenges, and it is worth doing yourself.github.com/type-challenges/type-challenges is a community-maintained set of type-level problems, from easy to extreme. Each one runs in the Playground and ships with test cases, so you get an answer immediately. The MyOmit and DeepReadonly you just wrote are both problems from this set.
The Handbook under typescriptlang.org/docs is the primary source. Every concept in this course has an authoritative version there. At your current level it is readable, which is the main thing this course was for.
typescriptlang.org/play needs no account, produces a shareable link, lets you switch TypeScript versions, and shows the compiled output. When you are unsure how something behaves, do not guess. Paste it in and read the answer. That habit is worth more than any single fact in this course.
The whole course on one map
Twelve chapters, five stages, one sentence each. If a sentence does not feel solid, open that chapter again.
Practice
An idea only counts once you have used it: the three forms bench, a validator written by hand, and two type-level exercises.
Final quiz
Twelve questions across the whole course: inference, narrowing, structural typing, generics, utility types, type operators, and tsconfig.
What does TypeScript infer for let a = "Oolong Tea"; and const b = "Oolong Tea";?
An order status is modelled as a discriminated union, and the default branch of the switch contains const x: never = order;. Three months later someone adds a new status, "refunded". What happens?
order({ size: "large", ice: "none" }) reports that ice does not exist, but storing the same object in a variable first and passing the variable compiles. Why?
Given function longest<T extends { length: number }>(a: T, b: T): T, which call is an error?
You are adding drafts: a half-filled Order must also be saveable, and every field may be empty for now. Which utility type is meant for this?
const MENU = { oolong: 12, mango: 22 } as const;type Name = keyof typeof MENU; — what is Name? Join the members with |.
Under strict, what is the type of e in catch (e), and why?
You are writing a theme config object. You want the compiler to check that it matches Config, and you also want config.theme to keep the literal type "dark" so later checks stay precise. Which form do you use?
Which of these statements about any and unknown are correct? (Choose all that apply.)
With verbatimModuleSyntax on, you need to import Order, which is used only as a type. Which form is correct?
const config = { theme: "dark" } as const; — what is the type of config.theme?
Last question. In the compiled code that runs in production, can if (order instanceof Order) test whether a value matches an interface named Order?
Where to go next
The course ends here. The part that makes it stick happens outside the course.
You have finished the course
In the prologue you were asking what types are for, since JavaScript already runs. Now you can read every word of a compiler error, design a validation boundary for data that comes back from a request, use a discriminated union so that illegal states cannot be written down, build Omit yourself, and plan a step-by-step migration for an old JavaScript project. That is twelve chapters of progress.
Start a project with Vite and TypeScript, and implement the ordering system this course kept coming back to: MenuItem, an Order as a discriminated union, a generic container, a Partial draft, and isOrder at the boundary. Turn on strict, and add noUncheckedIndexedAccess as well.
You will notice that all the types in this course were describing one world the whole time.
Work through type-challenges starting from the easy set, one problem a day. When you cannot solve one, read the discussion thread; the community posts some very clever solutions there. Once easy and medium are done, the type definitions in open-source libraries stop being intimidating.
Open the JavaScript library you use most and read its .d.ts inside node_modules. Then look at DefinitelyTyped to see how @types packages add types to libraries that have none. Reading other people's type design comes before doing your own.
A project you built and a pull request you sent say more than a certificate.
One last thing. What this course taught is not syntax. It is a way of looking at a program: every value has a shape, and a shape can be described, checked, and derived. The assumptions you carry in your head — "this will not be null here", "in this state that field must exist" — can be written down instead, and handed to a checker that never gets tired and never forgets.
It will not catch everything. Types are erased, the system is not fully sound, and the data arriving from outside is still your responsibility. What it does catch, it catches early, while you are still looking at the code that caused it.
Whatever language you write next, you will start by asking what the agreements are. That habit is what this course was really for.
- A type is an agreement written down. The compiler keeps the agreements you wrote; the ones you only remembered depend on luck.
- The three forms: an annotation checks but widens,
asneither checks nor keeps the literal,satisfiesdoes both. Addas constwhen you need the literals kept exactly. - Types are erased at compile time, so nothing is checked at runtime. Take external data in as
unknownand narrow it with a check you wrote, so that a failure happens at the boundary rather than three files away. - The type system is deliberately not fully sound.
any, assertions, array covariance, and unchecked indexing are real holes, so "it compiles" is not the same as "it is correct". anyis legitimate during a migration and at a genuinely dynamic boundary. The rule is containment: messy inside is fine, exported signatures must be precise. Reach forunknownfirst.- The most useful thing types do is modeling: a discriminated union makes illegal states impossible to write, so the agreement stops being a comment. And when you are unsure, check it in the Playground instead of guessing.