21. Effective TypeScript (2nd ed.) — crosswalk and gap closure
Dan Vanderkam, Effective TypeScript: 83 Specific Ways to Improve Your TypeScript, 2nd edition (May 2024) — 83 numbered Items across 10 chapters. This chapter maps every one onto the rest of this guide, then closes the gaps.
As with chapter 20, this is a companion to the book, not a replacement. Claims are in my own words, Items are cited by number so you can read the original, and where the guide already covers something this chapter says so and moves on.
Contents
- 1. The verdict at a glance
- 2. The full coverage matrix
- 3. Where the book is now wrong: TypeScript 6 is strict by default
- 4. The gaps, closed
- 5. The high-priority partials
- 6. Where the guide and the book independently agree
- 7. Reading order if you own the book
Every diagnostic quoted here came out of TypeScript 6.0.3 on this machine. The
demo files are in verification/books/ts/, one per Item, and each is reproducible:
cd verification/books/ts
for f in *.ts; do echo "== $f"; tsc --noEmit --strict --target es2022 "$f"; done
The error codes are worth learning; they are the fastest way to search for what a message actually means.
1. The verdict at a glance
| Verdict | Items | Share | What it means for you |
|---|---|---|---|
| covered | 28 | 34% | The guide already makes this point at comparable or greater depth. Skip. |
| partial | 36 | 43% | The guide states the rule but misses the mechanism or the framing. Read the Item. |
| gap | 10 | 12% | Genuinely absent. Closed in §4. |
| diverges | 2 | 2% | The book’s advice is stale against TypeScript 6. See §3. |
| skip | 7 | 8% | Editor setup, devDependencies, TSDoc, naming. |
The distribution here is the interesting result, and it is not flattering to the
guide. Coverage is strong exactly where the guide went deep — chapters 5 and 6 of
the book (any and unsoundness; generics and type-level programming) are almost
entirely covered, because 03-typescript-type-system.md already does infer,
distributive conditionals, homomorphic mapped types, variadic tuples, branded types
and a compiler-verified Equals<X, Y> harness.
The gaps are concentrated in the book’s first four chapters — the load-bearing mental models. Types as sets of values. The type-space / value-space divide. Excess property checking. Contextual typing. Widening. These are the things the book leads with, and the guide skipped straight past them to the advanced material. If you only read one part of this chapter, read §4.
2. The full coverage matrix
| # | Item | Verdict | Where it lives in the guide | Pri |
|---|---|---|---|---|
| 1 | Understand the Relationship Between TypeScript and JavaScript | partial | 02-javascript-core.md; 03-typescript-type-system.md | H |
| 2 | Know Which TypeScript Options You’re Using | diverges | 03-typescript-type-system.md § TS 6.0 to TS 7.0 | H |
| 3 | Understand That Code Generation Is Independent of Types | partial | 03-typescript-type-system.md § erasableSyntaxOnly; 02-javascript-core.md | H |
| 4 | Get Comfortable with Structural Typing | covered | 03-typescript-type-system.md § Structural typing; 11-design-patterns-typescript.md; 12-design-patterns-python.md § Protocol | H |
| 5 | Limit Use of the any Type | covered | 03-typescript-type-system.md (46 mentions of unknown/any); 14-cheatsheets.md | H |
| 6 | Use Your Editor to Interrogate and Explore the Type System | skip | 03-typescript-type-system.md (// ^? used throughout) | L |
| 7 | Think of Types as Sets of Values | gap | — | H |
| 8 | Know How to Tell Whether a Symbol Is in the Type Space or Value Space | gap | — | H |
| 9 | Prefer Type Annotations to Type Assertions | covered | 03-typescript-type-system.md § satisfies vs annotation vs assertion | H |
| 10 | Avoid Object Wrapper Types (String, Number, Boolean, Symbol, BigInt) | partial | 02-javascript-core.md § boxing and abstract equality | M |
| 11 | Distinguish Excess Property Checking from Type Checking | partial | 03-typescript-type-system.md | H |
| 12 | Apply Types to Entire Function Expressions When Possible | partial | 03-typescript-type-system.md; 11-design-patterns-typescript.md § satisfies-typed strategy maps | M |
| 13 | Know the Differences Between type and interface | partial | 03-typescript-type-system.md | H |
| 14 | Use readonly to Avoid Errors Associated with Mutation | covered | 03-typescript-type-system.md (33 mentions); 11-design-patterns-typescript.md (52); § structural sharing | H |
| 15 | Use Type Operations and Generic Types to Avoid Repeating Yourself | covered | 03-typescript-type-system.md § 25 utility types, compiler-verified; § homomorphic mapped types | H |
| 16 | Prefer More Precise Alternatives to Index Signatures | gap | 03-typescript-type-system.md (one passing mention) | H |
| 17 | Avoid Numeric Index Signatures | gap | — | M |
| 18 | Avoid Cluttering Your Code with Inferable Types | partial | 03-typescript-type-system.md | M |
| 19 | Use Different Variables for Different Types | skip | — | L |
| 20 | Understand How a Variable Gets Its Type | partial | 03-typescript-type-system.md § satisfies vs annotation vs assertion; § as const | H |
| 21 | Create Objects All at Once | partial | 11-design-patterns-typescript.md | M |
| 22 | Understand Type Narrowing | covered | 03-typescript-type-system.md § narrowing (25 mentions); 11-design-patterns-typescript.md § discriminated-union state machines | H |
| 23 | Be Consistent in Your Use of Aliases | partial | 03-typescript-type-system.md | M |
| 24 | Understand How Context Is Used in Type Inference | gap | — | H |
| 25 | Understand Evolving Types | gap | — | M |
| 26 | Use Functional Constructs and Libraries to Help Types Flow | partial | 11-design-patterns-typescript.md; 05-data-structures-typescript.md | M |
| 27 | Use async Functions Instead of Callbacks to Improve Type Flow | covered | 02-javascript-core.md § Promises/A+ and the event loop; 11-design-patterns-typescript.md § pMap/retry/circuit breaker | H |
| 28 | Use Classes and Currying to Create New Inference Sites | partial | 03-typescript-type-system.md § NoInfer; 11-design-patterns-typescript.md | M |
| 29 | Prefer Types That Always Represent Valid States | covered | 11-design-patterns-typescript.md § discriminated-union state machines (15 mentions); § Result/Either | H |
| 30 | Be Liberal in What You Accept and Strict in What You Produce | covered | 11-design-patterns-typescript.md; 03-typescript-type-system.md § variance | H |
| 31 | Don’t Repeat Type Information in Documentation | skip | — | L |
| 32 | Avoid Including null or undefined in Type Aliases | partial | 11-design-patterns-typescript.md | M |
| 33 | Push Null Values to the Perimeter of Your Types | partial | 11-design-patterns-typescript.md § Result/Either | H |
| 34 | Prefer Unions of Interfaces to Interfaces with Unions | covered | 11-design-patterns-typescript.md § discriminated unions; 03-typescript-type-system.md | H |
| 35 | Prefer More Precise Alternatives to String Types | covered | 03-typescript-type-system.md § template-literal types; § branded types; 11-design-patterns-typescript.md | H |
| 36 | Use a Distinct Type for Special Values | partial | 11-design-patterns-typescript.md; 02-javascript-core.md | H |
| 37 | Limit the Use of Optional Properties | partial | 11-design-patterns-typescript.md | H |
| 38 | Avoid Repeated Parameters of the Same Type | partial | 11-design-patterns-typescript.md | M |
| 39 | Prefer Unifying Types to Modeling Differences | partial | 11-design-patterns-typescript.md | M |
| 40 | Prefer Imprecise Types to Inaccurate Types | gap | — | H |
| 41 | Name Types Using the Language of Your Problem Domain | skip | — | L |
| 42 | Avoid Types Based on Anecdotal Data | partial | 03-typescript-type-system.md | M |
| 43 | Use the Narrowest Possible Scope for any Types | covered | 03-typescript-type-system.md | H |
| 44 | Prefer More Precise Variants of any to Plain any | covered | 03-typescript-type-system.md | M |
| 45 | Hide Unsafe Type Assertions in Well-Typed Functions | covered | 03-typescript-type-system.md § unsoundness; 05-data-structures-typescript.md | H |
| 46 | Use unknown Instead of any for Values with an Unknown Type | covered | 03-typescript-type-system.md (46 mentions) | H |
| 47 | Prefer Type-Safe Approaches to Monkey Patching | gap | 03-typescript-type-system.md § module augmentation (mechanism only) | M |
| 48 | Avoid Soundness Traps | covered | 03-typescript-type-system.md § unsoundness; § array covariance is unsound; § method bivariance vs property contravariance | H |
| 49 | Track Your Type Coverage to Prevent Regressions in Type Safety | gap | — | M |
| 50 | Think of Generics as Functions Between Types | covered | 03-typescript-type-system.md § conditional types; § generics | H |
| 51 | Avoid Unnecessary Type Parameters | partial | 03-typescript-type-system.md | M |
| 52 | Prefer Conditional Types to Overload Signatures | covered | 03-typescript-type-system.md § distributive conditional types | H |
| 53 | Know How to Control the Distribution of Unions over Conditional Types | covered | 03-typescript-type-system.md § distributive conditional types; § Equals<X,Y> via deferred conditionals | H |
| 54 | Use Template Literal Types to Model DSLs and Relationships Between Strings | partial | 03-typescript-type-system.md § template-literal types; 02-javascript-core.md | H |
| 55 | Write Tests for Your Types | covered | 03-typescript-type-system.md § 25 utility + 8 semantic assertions, compiler-verified; § Equals<X,Y> | H |
| 56 | Pay Attention to How Types Display | partial | 03-typescript-type-system.md | M |
| 57 | Prefer Tail-Recursive Generic Types | covered | 03-typescript-type-system.md § recursion depth | M |
| 58 | Consider Codegen as an Alternative to Complex Types | partial | 03-typescript-type-system.md | M |
| 59 | Use Never Types to Perform Exhaustiveness Checking | covered | 03-typescript-type-system.md § never (11 mentions); 11-design-patterns-typescript.md § discriminated-union state machines | H |
| 60 | Know How to Iterate Over Objects | partial | 05-data-structures-typescript.md (Object.entries) | H |
| 61 | Use Record Types to Keep Values in Sync | partial | 03-typescript-type-system.md § Record; 11-design-patterns-typescript.md § satisfies-typed strategy maps | H |
| 62 | Use Rest Parameters and Tuple Types to Model Variadic Functions | covered | 03-typescript-type-system.md § variadic tuples | M |
| 63 | Use Optional Never Properties to Model Exclusive Or | partial | 03-typescript-type-system.md; 11-design-patterns-typescript.md § discriminated unions | H |
| 64 | Consider Brands for Nominal Typing | covered | 03-typescript-type-system.md § branded/nominal types (12 mentions); 11-design-patterns-typescript.md (15) | H |
| 65 | Put TypeScript and @types in devDependencies | skip | — | L |
| 66 | Understand the Three Versions Involved in Type Declarations | partial | 03-typescript-type-system.md | M |
| 67 | Export All Types That Appear in Public APIs | partial | 11-design-patterns-typescript.md | M |
| 68 | Use TSDoc for API Comments | skip | — | L |
| 69 | Provide a Type for this in Callbacks if It’s Part of Their API | covered | 02-javascript-core.md § [[HomeObject]] and this-binding; 03-typescript-type-system.md § ThisType | M |
| 70 | Mirror Types to Sever Dependencies | partial | 11-design-patterns-typescript.md | M |
| 71 | Use Module Augmentation to Improve Types | covered | 03-typescript-type-system.md § module augmentation | M |
| 72 | Prefer ECMAScript Features to TypeScript Features | covered | 03-typescript-type-system.md § erasableSyntaxOnly; § TS 6.0/7.0; 16-testing-node-test.md § type stripping (the enum failure) | H |
| 73 | Use Source Maps to Debug TypeScript | gap | — | M |
| 74 | Know How to Reconstruct Types at Runtime | partial | 03-typescript-type-system.md (Zod mentioned); 19-testing-cheatsheet.md | H |
| 75 | Understand the DOM Hierarchy | skip | 03-typescript-type-system.md (two mentions) | L |
| 76 | Create an Accurate Model of Your Environment | partial | 03-typescript-type-system.md; 02-javascript-core.md § CJS vs ESM | M |
| 77 | Understand the Relationship Between Type Checking and Unit Testing | covered | 16-testing-node-test.md; 18-testing-strategy.md § coverage is not verification | H |
| 78 | Pay Attention to Compiler Performance | partial | 03-typescript-type-system.md § TS 7.0 (tsgo, ~10x) | M |
| 79 | Write Modern JavaScript | partial | 02-javascript-core.md § ES2026 iterator helpers; § optional chaining/nullish | M |
| 80 | Use @ts-check and JSDoc to Experiment with TypeScript | partial | 03-typescript-type-system.md | M |
| 81 | Use allowJs to Mix TypeScript and JavaScript | partial | 03-typescript-type-system.md | M |
| 82 | Convert Module by Module Up Your Dependency Graph | partial | 02-javascript-core.md § module graphs | M |
| 83 | Don’t Consider Migration Complete Until You Enable noImplicitAny | diverges | 03-typescript-type-system.md § TS 6.0/7.0 (strict is now default) | H |
3. Where the book is now wrong: TypeScript 6 is strict by default
Two Items are stale, and they are stale in the same direction.
Item 2 — “Know Which TypeScript Options You’re Using” and Item 83 — “Don’t
Consider Migration Complete Until You Enable noImplicitAny.” Both were written
against TypeScript ~5.4 and both treat strictness as something you opt into:
noImplicitAny is framed as the finish line of a migration.
On TypeScript 6.0.3, with no tsconfig.json and no flags at all:
$ cat implicit.ts
export function greet(name) { return "hi " + name; }
$ tsc --noEmit implicit.ts
implicit.ts(1,23): error TS7006: Parameter 'name' implicitly has an 'any' type.
$ tsc --noEmit --noImplicitAny false implicit.ts
$ echo $?
0
strictNullChecks behaves the same way — TS18047: 's' is possibly 'null' fires
with no configuration whatsoever.
So the advice inverts. noImplicitAny is now the starting line. A migration
opts out of it, and the finish line is deleting the opt-out. If you are reading
Item 83 as a checklist, read it backwards.
Five options the book’s examples take for granted are also on the way out. Each one now produces a hard error unless you explicitly acknowledge the deprecation, and each is documented as ceasing to function in TypeScript 7:
| option | diagnostic on 6.0.3 |
|---|---|
--target es5 | TS5107: Option 'target=ES5' is deprecated and will stop functioning in TypeScript 7.0 |
--module amd | TS5107 |
--moduleResolution node10 | TS5107 |
--downlevelIteration | TS5101 |
--baseUrl . | TS5101 |
The silencing incantation, if you need it in the meantime, is
"ignoreDeprecations": "6.0".
This connects to the guide’s existing TS 6 → TS 7 material in
03-typescript-type-system.md: the Go port (tsgo) and its order-of-magnitude
speed claim mean Item 78 (“Pay Attention to Compiler Performance”) is the one
Item where the guide is ahead of the book rather than behind it. The book’s
advice there predates the rewrite; treat its specifics as historical and measure
your own project.
4. The gaps, closed
Ten Items are genuinely absent. Seven of them are in the book’s first four chapters
and are, in my judgement, the most valuable pages in the book for someone who
already knows the advanced material. Demo files are in verification/books/ts/.
4.1 Item 7 — types are sets of values
This is the mental model the rest of the book is built on, and it is the single biggest omission from the guide. A type is the set of values it admits:
| type | as a set |
|---|---|
never | ∅ — the empty set |
'a' (literal) | a singleton |
'a' | 'b' | set union |
A & B | set intersection |
unknown | the universe |
And then the payoff: assignability is subset-hood. A is assignable to B
exactly when A’s set of values is a subset of B’s. Every confusing assignability
error becomes obvious once you read it that way.
declare let ab: 'a' | 'b';
const one: 'a' = ab; // {'a','b'} ⊄ {'a'}
declare let u: unknown;
declare let nothing: never;
const toUnknown: unknown = nothing; // ∅ ⊆ everything — always fine
const fromUnknown: number = u; // universe ⊄ number
type Impossible = string & number; // intersection of disjoint sets
const proof: never = 0 as Impossible; // compiles: Impossible collapsed to never
i07_sets.ts(5,7): error TS2322: Type '"a" | "b"' is not assignable to type '"a"'.
Type '"b"' is not assignable to type '"a"'.
i07_sets.ts(10,7): error TS2322: Type 'unknown' is not assignable to type 'number'.
Two things fall out of this that are worth keeping. First, never being assignable
to everything and from nothing is not a special case — it is what “empty set”
means, and it is why the exhaustiveness check in 03-typescript-type-system.md
works at all. Second, string & number is never, not an error, because
intersecting disjoint sets gives the empty set.
A note on writing this demo. My first attempt used
const ab: 'a' | 'b' = 'a'and reported no error. That was not a compiler quirk — control-flow analysis had narrowedabto the literal'a', so the assignment really was legal.declareis what defeats narrowing and lets you see the declared type. Worth knowing when you are trying to demonstrate a widening rule to someone.
4.2 Item 8 — type space and value space
The same identifier can name a type and a value, independently, with no conflict. This is the root of a large fraction of confusing TypeScript errors.
interface Person { name: string } // TYPE space only
const Person = { name: 'ada' }; // VALUE space only — no clash
type T1 = Person; // the interface
const v1 = Person; // the object
class Widget { id = 1 } // introduces BOTH
type WT = Widget; // the instance type
type Ctor = typeof Widget; // the constructor's type
const oops: Widget = Widget; // error: constructor ≠ instance
i08_space.ts(16,22): error TS2741: Property 'id' is missing in type 'typeof Widget'
but required in type 'Widget'.
The rules worth memorising:
interfaceandtypeintroduce only a type.const/let/functionintroduce only a value.classandenumintroduce both, which is why they are the ones that confuse people.typeofmeans different things on each side of the divide: in a value position it is the JavaScript operator returning"string"; in a type position it means “the type of this value”.- For a class, the value is the constructor and the type is the instance. That error message above — “Property ‘id’ is missing in type ‘typeof Widget’” — is precisely this distinction, and it is unreadable until you know the rule.
4.3 Item 16 — index signatures erode safety like any
An index signature says every key is valid, which means typos typecheck and the editor stops helping:
interface Loose { [k: string]: string }
declare const cfg: Loose;
const a = cfg.tiemout; // no error — the typo typechecks
interface Tight { timeout: string }
declare const t: Tight;
const c = t.tiemout; // this errors
i16_idxsig.ts(8,13): error TS2551: Property 'tiemout' does not exist on type
'Tight'. Did you mean 'timeout'?
The contrast is sharper than “you lose some safety”. With a real interface the
compiler not only catches the typo, it tells you the fix — TS2551 carries a
“Did you mean” suggestion. An index signature forfeits that entirely: one error
becomes zero errors and zero suggestions.
The alternatives, in rough order of preference: a real interface when you know the
keys; Record<Keys, V> when the keys are a union (see Item 61 in §5); and Map
when the keys are genuinely dynamic and open-ended — which also gets you a real
.size, iteration order guarantees, and non-string keys.
4.4 Item 17 — numeric index signatures are a fiction, and a leaky one
Object keys are strings at runtime. number as an index signature is a
TypeScript-only construct, and the boundary is odd enough to be worth knowing:
const o = { 1: 'one' };
const keys: string[] = Object.keys(o); // the 1 came back as "1"
interface NumIdx { [k: number]: string }
declare const n: NumIdx;
const byNumber = n[0]; // ok
const byNumericString = n['0']; // ok — a numeric-LOOKING string literal passes
const byWord = n['nope']; // error
i17_numidx.ts(10,18): error TS7015: Element implicitly has an 'any' type because
index expression is not of type 'number'.
I expected n['0'] to be rejected and it is not — TypeScript accepts a string
literal that looks numeric, and rejects one that does not. Prefer Array, a tuple,
ArrayLike, or Iterable and you never have to hold this rule in your head.
4.5 Item 24 — contextual typing
TypeScript infers from the surrounding context, not only from the initialiser. The consequence catches everyone at least once: extracting a value into a variable can change its type and break a call that worked inline.
type Dir = 'north' | 'south';
declare function move(d: Dir): void;
move('north'); // fine: contextually typed as Dir
const d = 'north'; // no context — but `const` keeps the literal type
move(d); // fine
let e = 'north'; // no context, and `let` widens to string
move(e); // error
const o = { dir: 'north' }; // property widens even under const
move(o.dir); // error
const o2 = { dir: 'north' } as const;
move(o2.dir); // fine
i24_context.ts(8,6): error TS2345: Argument of type 'string' is not assignable to
parameter of type 'Dir'.
i24_context.ts(11,6): error TS2345: Argument of type 'string' is not assignable to
parameter of type 'Dir'.
Note which two lines errored and which two did not. The const-bound variable
survives; the const-bound object property does not, because the property is
still mutable. That pair is the whole rule in four lines, and it is the reason
as const exists.
The fixes are as const, an explicit annotation (const d: Dir = 'north'), or
keeping the value inline. This is the same widening story as Item 20 seen from the
other side, and together they explain most “but it worked when I inlined it”
confusion. The guide covers as const and satisfies as tools without ever
explaining the mechanism they exist to control — which is why this Item and Item 20
are both worth your time even though the guide’s satisfies material is good.
4.6 Item 25 — evolving types, the one place a type widens
Almost everything in TypeScript narrows. Values initialised to null, undefined
or [] are the exception: they get an implicit-any type that is allowed to grow
as you assign to it. The boundary is where the implicit any would escape.
function pushesFirst(start: number, limit: number) {
const nums = []; // implicit any[], allowed to evolve
for (let i = start; i < limit; i++) nums.push(i);
return nums; // evolved to number[]
}
function lengthIsFine() {
const a = [];
return a.length; // NO error — the any does not escape
}
function returningItEscapes() {
const a = [];
return a; // error
}
i25_evolving.ts(16,9): error TS7034: Variable 'a' implicitly has type 'any[]' in
some locations where its type cannot be determined.
i25_evolving.ts(17,10): error TS7005: Variable 'a' implicitly has an 'any[]' type.
Note where the line is drawn: a.length on a never-populated evolving array is
fine, because the result is number and the any stays contained. It errors
only when the unresolved any[] would leak into a signature or be read as a value.
I got this wrong on the first attempt — I assumed .length would error, wrote that
down, and the compiler disagreed. The pair TS7034 + TS7005 is the signature to
recognise.
4.7 Item 40 — prefer imprecise types to inaccurate ones
The most useful piece of judgement in the book, and entirely absent from the guide.
There is an uncanny valley of type safety: a complex type that is subtly wrong
is worse than a simple type that is honestly vague. The elaborate type produces
confusing errors on correct code, teaches people to reach for as any, and costs
you the credibility to add real types later. The guide’s whole ch.3 pushes toward
maximum precision and never says where to stop, which is a real omission in a
document that also contains a 54-mention section on infer.
The rule: if you cannot model something accurately, model it loosely and document
the gap. unknown at a boundary plus one validation function beats a
half-right conditional type.
4.8 Items 47, 49, 73 — the remaining three
| Item | What to know |
|---|---|
| 47 monkey patching | Attaching data to built-ins or DOM nodes defeats the checker. If you must, use declaration merging or assert a custom interface — and note that declare global applies to the whole program, not just your file, so it is not a local decision. The guide covers module augmentation as a mechanism but never this use of it. |
| 49 type coverage | Measure the share of expressions with a non-any type, and track it, so the anys you inherited cannot quietly multiply. The type-coverage tool exists for this. A CI gate on this number is the cheapest way to make a migration monotonic. |
| 73 source maps | Debug the TypeScript you wrote, not the JavaScript that was emitted. Worth knowing that source maps also need to reach production for stack traces to be readable, and that Node’s own type-stripping (covered in ch.16) changes the picture. |
5. The high-priority partials
Thirteen Items where the guide has the rule and is missing the part that makes the Item worth reading. These are the best value per page in the book for you.
5.1 Item 11 — excess property checking is a separate rule
This is a favourite interview question precisely because the asymmetry looks like a compiler bug.
interface Opts { title: string }
const direct: Opts = { title: 'a', extra: 1 }; // error
const tmp = { title: 'a', extra: 1 };
const viaVar: Opts = tmp; // NO error
function take(o: Opts) {}
take({ title: 'a', extra: 1 }); // error
take(tmp); // NO error
i11_excess.ts(4,36): error TS2353: Object literal may only specify known
properties, and 'extra' does not exist in type 'Opts'.
i11_excess.ts(10,20): error TS2353: Object literal may only specify known
properties, and 'extra' does not exist in type 'Opts'.
Excess property checking is not part of assignability. It is an extra check that fires only on fresh object literals, and assigning through a variable launders the freshness away. Structural typing genuinely permits the extra property; the literal check exists to catch typos in the common case where you clearly meant to write exactly that type. Once you can say that sentence, the behaviour stops being surprising.
5.2 Item 13 — type versus interface
One of the two or three most-asked TypeScript interview questions, and the guide has three passing mentions. The differences that actually matter:
-
Declaration merging —
interfaceonly. Twointerface Foodeclarations merge; twotype Fooare a duplicate-identifier error. This is what makes module augmentation (Item 71) work, and it is also an argument againstinterfacefor a closed type you do not want extended from elsewhere. -
Unions, mapped types, conditional types, tuples —
typeonly. An interface cannot be a union. -
interfacecanextends(and gets a cached, cheaper assignability check);typecomposes with&, which behaves differently on conflicting members. This one is worth seeing rather than taking on trust:interface A { x: string } interface BadExtends extends A { x: number } // errors type Inter = { x: string } & { x: number }; // no error HERE declare const i: Inter; const probe: never = i.x; // compiles — so i.x really is neveri13_conflict.ts(2,11): error TS2430: Interface 'BadExtends' incorrectly extends interface 'A'. Types of property 'x' are incompatible. Type 'number' is not assignable to type 'string'.extendsrefuses the conflict at the point of declaration.&accepts it silently and quietly makes that propertynever— which you discover much later, at the first assignment that cannot possibly succeed. Theconst probe: neverline is the proof: it only compiles becausei.xis genuinelynever. -
Error messages tend to be shorter and name the interface, where a type alias is often expanded structurally.
Reasonable default: interface for object shapes that describe an API,
type for everything else.
5.3 Item 20 — how a variable gets its type
flowchart TD
A["Variable declaration"] --> B{"let or const?"}
B -->|"let"| C["Widened to general type<br/>(string, number, boolean)"]
B -->|"const"| D{"Primitive or object/array?"}
D -->|"primitive"| E["Literal type<br/>(e.g. 'x')"]
D -->|"object/array"| F["Properties still widen<br/>(fields remain mutable)"]
F --> G{"Annotated with 'as const'?"}
G -->|"no"| H["Property widens to string<br/>{ k: string }"]
G -->|"yes"| I["Frozen literal type<br/>readonly { k: 'v' }"]
let l = 'x'; // widened to string
const c = 'x'; // literal type 'x'
const arr = [1, 2]; // number[]
const tup = [1, 2] as const; // readonly [1, 2]
let obj = { k: 'v' }; // { k: string } — the PROPERTY widens
i20_widen.ts(7,7): error TS2322: Type 'string' is not assignable to type '"x"'.
i20_widen.ts(12,7): error TS2322: Type '{ k: string; }' is not assignable to
type '{ k: "v"; }'.
Types of property 'k' are incompatible.
Type 'string' is not assignable to type '"v"'.
The rule that trips people: const prevents rebinding, not property widening.
const obj = { k: 'v' } still gives { k: string }, because the property is
mutable. as const is what freezes it. Read this with Item 24 (§4.5) — they are the
same mechanism from two directions.
5.4 Item 60 — for...in widens the key
interface ABC { a: number; b: string; c: boolean }
declare const abc: ABC;
for (const k in abc) {
const v = abc[k]; // error
}
i60_forin.ts(6,13): error TS7053: Element implicitly has an 'any' type because
expression of type 'string' can't be used to index type 'ABC'.
No index signature with a parameter of type 'string' was found on type 'ABC'.
for...in types the key as string, not keyof ABC — and this is correct,
because at runtime the object may carry inherited or extra keys the type does not
mention. The options: Object.keys(abc) as (keyof ABC)[] when you know better, or
Object.entries when you want values too and can live with a widened key. It is
worth being able to explain why the compiler is right here, because “TypeScript
is being annoying” is the wrong answer.
5.5 Item 61 — Record and the fail-open / fail-closed choice
The guide already uses satisfies-typed strategy maps, which is the right pattern.
What it never names is the reason: when the union of keys grows, does your code
fail open (silently handles nothing) or fail closed (stops compiling)?
Record<Union, Handler> fails closed. A partial lookup table fails open. Choosing
deliberately, and being able to say which you chose and why, is a genuinely useful
design vocabulary — it is the same instinct as the never exhaustiveness check in
Item 59, applied to data instead of control flow.
5.6 Item 63 — or is inclusive; optional-never models exclusive or
interface OnlyX { x: number; y?: never }
interface OnlyY { y: number; x?: never }
type Xor = OnlyX | OnlyY;
const both: Xor = { x: 1, y: 1 }; // error
interface LooseX { x: number }
interface LooseY { y: number }
const loose: LooseX | LooseY = { x: 1, y: 1 }; // NO error
i63_xor.ts(8,7): error TS2322: Type '{ x: number; y: number; }' is not assignable
to type 'Xor'.
Types of property 'y' are incompatible.
Type 'number' is not assignable to type 'undefined'.
A | B means A, or B, or both. The guide always reaches for a discriminant tag,
which is the better answer when you control the data — but you often do not, and
?: never is how you enforce exclusivity without one. The bottom half of that
snippet is the part to remember: without the never guards, the both-properties
object is perfectly legal.
5.7 Items 1, 3, 33, 36, 37, 54, 74 — briefly
- Item 1 — TypeScript is a syntactic superset of JavaScript whose checker deliberately rejects some working code, on the grounds that the odd usage is more likely a mistake than an intention. The guide teaches JS semantics and TS types as separate subjects and never frames the relationship.
- Item 3 — code generation is independent of types, so a file with type errors
still emits. The guide covers erasure via
erasableSyntaxOnlybut never states this consequence, which is the one that matters in a build pipeline: a successful build is not a successful check. - Item 33 — push nullability to the perimeter: make a whole object nullable
rather than sprinkling optional fields, so one check at the boundary settles it for
the interior. The guide’s
Result/Eithermaterial solves the adjacent problem for errors and leaves this one for plain data. - Item 36 — a sentinel drawn from the value domain (
0,-1,"") is assignable where a real value is expected, so the checker cannot help you. Usenull/undefinedor a distinct type.indexOfreturning-1is the canonical example of getting this wrong. - Item 37 — optional properties multiply the states you must handle. The concrete
pattern the guide lacks: a loose input type and a strict internal type,
normalised once at the boundary, so the interior never carries
undefined. - Item 54 — the guide teaches template-literal-type syntax but not the
DSL-modelling use, nor the key-remapping combination (
asclauses in mapped types) which is where the technique earns its keep. - Item 74 — types are erased, so the type system stops at the I/O boundary. The guide names Zod once and never presents the general problem or the menu of solutions (schema library, generated validators, hand-written guards). This is a routine design question in real work and a plausible interview one.
6. Where the guide and the book independently agree
Three places worth noting, because independent convergence is evidence and because they are nice things to be able to cite.
Item 55 — “Write Tests for Your Types.” The guide’s 03-typescript-type-system.md
independently arrived at the same Equals<X, Y> deferred-conditional harness the
book recommends, and it documents the failure mode the book does not: an
Expect<A, B> = Assert<Equals<A, B>> alias does not work, because the generic
alias defers to boolean and you get “Type ‘Equals<A, B>’ does not satisfy
‘true’”. You must use Assert<Equals<X, Y>> inline at every assertion site. The
book’s advice plus the guide’s gotcha is strictly better than either.
Item 72 — “Prefer ECMAScript Features to TypeScript Features.” The guide hit this
from the other direction and empirically: a TypeScript enum in a node:test file
failed at runtime with ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX: TypeScript enum is not supported in strip-only mode, needing --experimental-transform-types. From the
compiler side, --erasableSyntaxOnly rejects the same construct with
TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled. Two
independent tools, one message: enums are not erasable, so stop using them.
Item 77 — “Understand the Relationship Between Type Checking and Unit Testing.” The book argues these are complementary and neither subsumes the other. The guide’s ch.18 has independent evidence for exactly that claim: two suites at identical 100% line coverage scoring 0/5 and 4/5 on mutation, plus (added in ch.20 §4) a unit-versus-integration experiment where both suites scored 11/15 while killing different mutants. Types eliminate whole classes of error across all inputs; tests demonstrate behaviour on chosen inputs. Neither number substitutes for the other.
7. Reading order if you own the book
- Chapters 1–4 in full. This is where the guide is weakest and the book is strongest. Items 7, 8, 11, 13, 16, 17, 20, 24, 25, 40 are the ten pages that will change how you read TypeScript errors.
- §3 of this chapter before Item 2 or Item 83, so you do not learn the old flag story.
- The high-priority partials (§5): Items 1, 3, 33, 36, 37, 54, 60, 61, 63, 74.
- Skim chapters 5 and 6. Almost entirely covered by
03-typescript-type-system.md; dip in only where the matrix says partial. - Chapters 8–10 are mostly
partialorskip— read them when you are actually publishing types or actually migrating a codebase, not now.
That is roughly 25 Items of real reading out of 83, plus a skim.
Previous: 20. Effective Python (3rd ed.) — crosswalk and gap closure
Verify it yourself
books/ts/i07_sets.ts
// Item 7: types as sets of values. Assignability IS subset-hood.
// NB: `declare` is needed - a `const` initialised inline gets narrowed by
// control-flow analysis, which hides the widening you are trying to show.
declare let ab: 'a' | 'b';
const one: 'a' = ab; // error: {'a','b'} is not a subset of {'a'}
declare let u: unknown; // the universe
declare let nothing: never; // the empty set
const toUnknown: unknown = nothing; // ok: empty set is a subset of everything
const fromUnknown: number = u; // error: the universe is not a subset of number
type Impossible = string & number; // intersection of disjoint primitive sets
const proof: never = 0 as Impossible; // ok: proves Impossible collapsed to never
books/ts/i08_space.ts
// Item 8: the same name can live in type space and value space independently.
interface Person { name: string } // TYPE space only
const Person = { name: 'ada' }; // VALUE space only - no conflict
type T1 = Person; // the interface
const v1 = Person; // the object
type TypeofPerson = typeof Person; // typeof in VALUE position -> the object's type
const badType: Person = 1 as any; // fine, Person is a type here
class Widget { id = 1 } // introduces BOTH a type and a value
type WT = Widget; // instance type
const WV = Widget; // the constructor
type Ctor = typeof Widget; // the constructor's type
const oops: Widget = Widget; // error: constructor is not an instance
books/ts/i11_excess.ts
// Item 11: excess property checking applies to object LITERALS only.
interface Opts { title: string }
const direct: Opts = { title: 'a', extra: 1 }; // error: excess property check
const tmp = { title: 'a', extra: 1 };
const viaVar: Opts = tmp; // NO error: plain assignability
function take(o: Opts) {}
take({ title: 'a', extra: 1 }); // error: literal at a call site
take(tmp); // NO error
books/ts/i13_conflict.ts
interface A { x: string }
interface BadExtends extends A { x: number } // claim: ERROR
type TA = { x: string };
type TB = { x: number };
type Inter = TA & TB; // claim: no error here
declare const i: Inter;
const probe: never = i.x; // claim: ok if i.x really is never
books/ts/i16_idxsig.ts
interface Loose { [k: string]: string }
declare const cfg: Loose;
const a = cfg.tiemout; // claim: NO error - the typo typechecks
const b = cfg.timeout;
interface Tight { timeout: string }
declare const t: Tight;
const c = t.tiemout; // claim: this DOES error
books/ts/i17_numidx.ts
// Item 17: object keys are strings at runtime; a number index signature is a
// TypeScript-only fiction - and the fiction is leakier than you would expect.
const o = { 1: 'one' };
const keys: string[] = Object.keys(o); // the 1 came back as "1"
interface NumIdx { [k: number]: string }
declare const n: NumIdx;
const byNumber = n[0]; // ok
const byNumericString = n['0']; // ok - TS accepts a numeric-LOOKING string literal
const byWord = n['nope']; // error: not a numeric key
books/ts/i20_widen.ts
// Item 20: how a variable gets its type - widening.
let l = 'x'; // widened to string
const c = 'x'; // literal type 'x'
const arr = [1, 2]; // number[]
const tup = [1, 2] as const; // readonly [1, 2]
const wantsLiteral: 'x' = l; // error: string is not 'x'
const okLiteral: 'x' = c; // ok
let obj = { k: 'v' }; // { k: string } - property widened even under const
const o2 = { k: 'v' } as const; // { readonly k: 'v' }
const needs: { k: 'v' } = obj; // error
const fine: { readonly k: 'v' } = o2;
books/ts/i24_context.ts
type Dir = 'north' | 'south';
declare function move(d: Dir): void;
move('north'); // claim: ok, contextually typed
const d = 'north';
move(d); // claim: ok, const keeps the literal type
let e = 'north';
move(e); // claim: ERROR, let widened to string
const o = { dir: 'north' };
move(o.dir); // claim: ERROR, property widened even under const
const o2 = { dir: 'north' } as const;
move(o2.dir); // claim: ok
books/ts/i25_evolving.ts
// Item 25: an evolving (implicit-any) array is the one place a type WIDENS
// instead of narrowing. The boundary is where the implicit any would escape.
function pushesFirst(start: number, limit: number) {
const nums = []; // implicit any[], allowed to evolve
for (let i = start; i < limit; i++) nums.push(i);
return nums; // evolved to number[]
}
const r: number[] = pushesFirst(0, 3);
function lengthIsFine() {
const a = [];
return a.length; // NO error: number, the any does not escape
}
function returningItEscapes() {
const a = [];
return a; // error: the implicit any[] would escape
}
function readingItEscapes() {
const a = [];
const first = a[0]; // error: reading an unresolved any[]
return first;
}
books/ts/i60_forin.ts
// Item 60: for-in widens the key to string; Object.entries keeps it narrow.
interface ABC { a: number; b: string; c: boolean }
declare const abc: ABC;
for (const k in abc) {
const v = abc[k]; // error: k is string, not keyof ABC
}
for (const k of Object.keys(abc) as (keyof ABC)[]) {
const v = abc[k]; // ok, via an explicit assertion
}
for (const [k, v] of Object.entries(abc)) {
const kk: string = k; // entries gives string keys and a union value
}
books/ts/i63_xor.ts
// Item 63: "or" is inclusive; optional-never models exclusive or.
interface OnlyX { x: number; y?: never }
interface OnlyY { y: number; x?: never }
type Xor = OnlyX | OnlyY;
const a: Xor = { x: 1 }; // ok
const b: Xor = { y: 1 }; // ok
const both: Xor = { x: 1, y: 1 }; // error: exclusivity enforced
// without the never guards, "both" is structurally legal:
interface LooseX { x: number }
interface LooseY { y: number }
const loose: LooseX | LooseY = { x: 1, y: 1 }; // no error