Learning: TypeScript
Become the engineer trusted to own a TypeScript codebase on a team: able to model a domain so the compiler rejects the states that should not exist, keep type assertions out of code that carries weight, validate what crosses a runtime boundary rather than assuming the types held, and review someone's types and say concretely why a clever generic is costing more than it gives.
Start here: 0001. Values and Coercion
Latest lesson: 0049. Trusted to Make the Call, which closes the arc
Success looks like
- Predict what JavaScript does with coercion, closures,
thisand the event loop, before running the code and without the type system's help. - Turn a set of illegal states into a type the compiler rejects, and prove exhaustiveness with
never. - Read a generic's error message to its cause instead of reaching for
as. - Say exactly what survives compilation and what erases, and validate every value entering the program from outside it.
- Configure the compiler with intent: name what each strictness flag buys and why the module and target settings are what they are.
- Write a generic function whose inference works, so callers annotate nothing.
- Publish a package whose types are usable by consumers, including the declaration output and the exports it advertises.
- Review a pull request and say precisely why a conditional type, an assertion, or an
enumis the wrong tool there.
Constraints
- Assumes no prior TypeScript, and no more JavaScript than the arc teaches. TypeScript is a type system over JavaScript, so JavaScript semantics are taught wherever they explain the behaviour a type cannot.
- The compile-time and runtime boundary is treated as a hard line throughout, because almost every serious TypeScript mistake is a claim about runtime that only the compiler ever checked.
- Needs only a JavaScript runtime and a terminal on any supported OS, with the online playground standing in for both when a rep only needs to see a type. No paid tooling, no cloud account, no framework.
- Reps are small programs and small type experiments that fit one sitting.
- TypeScript releases every few months and the type system gains real expressiveness in those releases. Version-sensitive claims are checked against the release notes, and any lesson that depends on a version says which one.
Out of scope
- Frameworks and UI libraries as subjects in their own right: React, Angular, Vue, Svelte, Next.js.
- Bundlers and monorepo tooling as subjects: webpack, Vite, esbuild, Turborepo. Module resolution is taught from the compiler's own model, which is what those tools have to agree with.
- Other runtimes as subjects: Deno, Bun, the browser platform. They appear only where behaviour genuinely differs.
- Node.js as a platform in its own right: streams, clustering, native addons.
- Type-level programming as sport. Mapped and conditional types are taught to the point where they serve a caller, and stage 6 says explicitly where that point is.
The arc
Seven stages, zero to senior. Not a lesson list: a stage takes several lessons, and the boundaries are soft.
| Stage | Lessons | Covers | Done when |
|---|---|---|---|
| 1. The JavaScript underneath | 0001 to 0007 | Values and coercion, objects and prototypes, closures, this, modules, the event loop, promises and async | Can predict what a program does at runtime, with no types involved |
| 2. Types over values | 0008 to 0014 | Primitives, arrays and tuples, unions, literal types, structural assignability, narrowing, function types, readonly, what inference already knows | Annotates only where inference cannot reach |
| 3. Strictness and the compiler | 0015 to 0021 | The strict family flag by flag, the strict-shaped flags strict still omits, unknown versus any, assertions versus declarations, tsconfig, module resolution, target and lib | Keeps strictness on and fixes what it reports instead of suppressing it, and turns on the checks strict leaves out |
| 4. Modelling | 0022 to 0028 | Discriminated unions, exhaustiveness with never, branded types, generics and constraints, satisfies, interface versus type alias | Illegal states are unrepresentable, and the compiler is what proves it |
| 5. The runtime boundary | 0029 to 0035 | What erases, declaration files, validating input at the edge, errors as values, consuming untyped and wrongly typed dependencies | No value enters the program unvalidated, and no assertion is load-bearing |
| 6. Type-level tools | 0036 to 0042 | Mapped and conditional types, infer, template literal types, variance and assignability, inference for library APIs, compiler performance | Writes an API whose types serve its callers, and stops before cleverness |
| 7. Judgment | 0043 to 0049 | Publishing a public type surface, what counts as a breaking change in types, review, reading the release notes and the compiler's own declarations | Trusted to make the call and to explain it to someone else |
Lessons
Work through these in order.
| # | Lesson | Teaches |
|---|---|---|
| 0001 | Values and Coercion | Seven primitives, two empties, and an equality operator that converts before comparing |
| 0002 | Objects Are References | const stops rebinding, spread copies one level, and readonly disappears at run time |
| 0003 | Prototypes and Classes | Property lookup walks a chain, and class syntax is one way to build that chain |
| 0004 | this Is Decided by the Call | this comes from how a function is called, so extracting a method throws it away |
| 0005 | Scope and Closures | A closure captures the binding rather than the value, and let makes one per iteration |
| 0006 | The Event Loop, Promises and await | Microtasks drain before the next timer, and two awaits in a row are sequential |
| 0007 | Modules | Imports are hoisted and bound live, and a file with no import or export is not a module |
| 0008 | The Types You Write | The annotation vocabulary, and a compiler that already objects before you configure it |
| 0009 | Tuples and readonly | A fixed length the compiler enforces on reads and forgets on push |
| 0010 | Unions and Literal Types | A value with more than one possible type, and the widening that throws the interesting one away |
| 0011 | Structural Assignability | Shape decides what goes where, except for the one check that only fires on a fresh object literal |
| 0012 | Narrowing | Control flow tells the compiler what a union has become, and exactly two things take it back |
| 0013 | Function Types | Parameters are checked in the direction you expect, unless you wrote a method |
| 0014 | What Inference Already Knows | Where inference reaches, so that writing an annotation means something when you do |
| 0015 | What strict Turns On | Seven checks behind one flag, each with a failure it exists to catch |
| 0016 | The Checks strict Leaves Out | Six more checks worth having, one that the compiler refuses to enable, and one that does nothing at all |
| 0017 | unknown Instead of any | One type accepts everything and lets you do nothing, the other accepts everything and checks nothing |
| 0018 | An Assertion Is Not a Check | Every way to tell the compiler to stop checking, and what each one costs when the claim is wrong |
| 0019 | Reading a tsconfig | Which files the compiler looks at, where the settings come from, and why the generated file says what it says |
| 0020 | target and lib | One setting changes what is emitted and the other changes what exists, and naming the second throws the default away |
| 0021 | Module Resolution | The same compiler options give opposite verdicts depending on one field in package.json |
| 0022 | Discriminated Unions | One shared literal property, and a union the compiler can take apart |
| 0023 | Exhaustiveness With never | Adding a member to a union should break every switch that ignored it, and one line makes it so |
| 0024 | Making Illegal States Unrepresentable | Count the states your type permits, then remove the ones your domain does not have |
| 0025 | Branded Types | Nominal typing on top of a structural system, and the single assertion it costs |
| 0026 | Generics and Constraints | A type parameter is a promise to the caller, and a constraint is what lets you keep it |
| 0027 | satisfies | Check a value against a type without letting the type replace what you wrote |
| 0028 | interface or type | One reports a conflicting member where you wrote it and the other reports it somewhere else |
| 0029 | Nothing Survives to Run Time | Every type is a claim about a program that no longer exists when the claim would matter |
| 0030 | unknown at the Edge | Find every place a value arrives from outside, and give each one the only honest type |
| 0031 | Parsing Instead of Asserting | One declaration that produces both the runtime check and the static type |
| 0032 | Type Predicates and Assertion Functions | A signature that promises a check, and a body nothing compares it against |
| 0033 | Errors as Values | A failure in the return type is one the compiler can make you handle |
| 0034 | Declaration Files | Types for code the compiler never sees, and what that promise is worth |
| 0035 | When the Declaration Lies | The compiler believes the declaration and the runtime has never heard of it |
| 0036 | Mapped Types | Transform every property of a type at once, and remove the ones you do not want |
| 0037 | Conditional Types | A type that branches, and a union it quietly takes apart first |
| 0038 | infer | Pull a type out of a position instead of asking the caller to name it |
| 0039 | Template Literal Types | A string pattern the compiler checks, and the key names it can build |
| 0040 | Variance and Assignability | Which direction a type may travel, and the array that gets it wrong on purpose |
| 0041 | Inference for Library APIs | Design the signature so the caller annotates nothing |
| 0042 | Knowing When to Stop | Measure what a clever type costs, then justify it to whoever maintains it |
| 0043 | What Counts as a Breaking Change | Which edits to a published type break a consumer, and which direction each one has to travel |
| 0044 | Publishing a Type Surface | What a consumer actually resolves when they import your package, and how to check it before they do |
| 0045 | The Consumer's Settings Are Not Yours | A strict consumer inherits a loose author's claims and has no way to notice |
| 0046 | The TypeScript That Does Not Erase | Four features that emit code you did not ask for, and the flag that names them |
| 0047 | Settling It From the Source | Which document answers a question about the compiler, and how to read the declarations it ships |
| 0048 | Reviewing TypeScript | Naming what a type costs, instead of saying it looks complicated |
| 0049 | Trusted to Make the Call | The last judgment in the arc, and what the compiler was never going to do for you |
Reference
- Glossary: canonical terms for this topic
- Resources: trusted sources, each annotated with what it covers
- Coercion and equality: falsy values, what
==converts, which default operator to write - Event loop and promises: queue ordering, combinators, and the async mistakes no compiler reports
- Types over values: what each annotation buys, where inference reaches, and the holes the compiler leaves open on purpose
- Strictness and the compiler: which flag catches which failure, where a setting comes from, and how an import resolves
- Modelling: which shape removes the illegal state, and which declaration catches the mistake where you made it
- The runtime boundary: where values enter, what checks them, and which claims nothing checks at all
- Type-level tools: which construct computes which type, and the test a clever one has to pass
- Judgment: which edits break a consumer, what does not erase, and how to argue the call from the source
How this works
Each lesson is short and self-contained. Answer keys are collapsed: recall first, then open them. The real-world reps matter more than the reading, and spacing them out is the point. Anything still unclear at the end of a lesson is worth chasing to its primary source before moving on.