Chapter 21

21. Effective TypeScript (2nd ed.) — crosswalk and gap closure

All 83 Effective TypeScript items mapped, with TS 6 divergences verified.

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

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

VerdictItemsShareWhat it means for you
covered2834%The guide already makes this point at comparable or greater depth. Skip.
partial3643%The guide states the rule but misses the mechanism or the framing. Read the Item.
gap1012%Genuinely absent. Closed in §4.
diverges22%The book’s advice is stale against TypeScript 6. See §3.
skip78%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

#ItemVerdictWhere it lives in the guidePri
1Understand the Relationship Between TypeScript and JavaScriptpartial02-javascript-core.md; 03-typescript-type-system.mdH
2Know Which TypeScript Options You’re Usingdiverges03-typescript-type-system.md § TS 6.0 to TS 7.0H
3Understand That Code Generation Is Independent of Typespartial03-typescript-type-system.md § erasableSyntaxOnly; 02-javascript-core.mdH
4Get Comfortable with Structural Typingcovered03-typescript-type-system.md § Structural typing; 11-design-patterns-typescript.md; 12-design-patterns-python.md § ProtocolH
5Limit Use of the any Typecovered03-typescript-type-system.md (46 mentions of unknown/any); 14-cheatsheets.mdH
6Use Your Editor to Interrogate and Explore the Type Systemskip03-typescript-type-system.md (// ^? used throughout)L
7Think of Types as Sets of ValuesgapH
8Know How to Tell Whether a Symbol Is in the Type Space or Value SpacegapH
9Prefer Type Annotations to Type Assertionscovered03-typescript-type-system.md § satisfies vs annotation vs assertionH
10Avoid Object Wrapper Types (String, Number, Boolean, Symbol, BigInt)partial02-javascript-core.md § boxing and abstract equalityM
11Distinguish Excess Property Checking from Type Checkingpartial03-typescript-type-system.mdH
12Apply Types to Entire Function Expressions When Possiblepartial03-typescript-type-system.md; 11-design-patterns-typescript.md § satisfies-typed strategy mapsM
13Know the Differences Between type and interfacepartial03-typescript-type-system.mdH
14Use readonly to Avoid Errors Associated with Mutationcovered03-typescript-type-system.md (33 mentions); 11-design-patterns-typescript.md (52); § structural sharingH
15Use Type Operations and Generic Types to Avoid Repeating Yourselfcovered03-typescript-type-system.md § 25 utility types, compiler-verified; § homomorphic mapped typesH
16Prefer More Precise Alternatives to Index Signaturesgap03-typescript-type-system.md (one passing mention)H
17Avoid Numeric Index SignaturesgapM
18Avoid Cluttering Your Code with Inferable Typespartial03-typescript-type-system.mdM
19Use Different Variables for Different TypesskipL
20Understand How a Variable Gets Its Typepartial03-typescript-type-system.md § satisfies vs annotation vs assertion; § as constH
21Create Objects All at Oncepartial11-design-patterns-typescript.mdM
22Understand Type Narrowingcovered03-typescript-type-system.md § narrowing (25 mentions); 11-design-patterns-typescript.md § discriminated-union state machinesH
23Be Consistent in Your Use of Aliasespartial03-typescript-type-system.mdM
24Understand How Context Is Used in Type InferencegapH
25Understand Evolving TypesgapM
26Use Functional Constructs and Libraries to Help Types Flowpartial11-design-patterns-typescript.md; 05-data-structures-typescript.mdM
27Use async Functions Instead of Callbacks to Improve Type Flowcovered02-javascript-core.md § Promises/A+ and the event loop; 11-design-patterns-typescript.md § pMap/retry/circuit breakerH
28Use Classes and Currying to Create New Inference Sitespartial03-typescript-type-system.md § NoInfer; 11-design-patterns-typescript.mdM
29Prefer Types That Always Represent Valid Statescovered11-design-patterns-typescript.md § discriminated-union state machines (15 mentions); § Result/EitherH
30Be Liberal in What You Accept and Strict in What You Producecovered11-design-patterns-typescript.md; 03-typescript-type-system.md § varianceH
31Don’t Repeat Type Information in DocumentationskipL
32Avoid Including null or undefined in Type Aliasespartial11-design-patterns-typescript.mdM
33Push Null Values to the Perimeter of Your Typespartial11-design-patterns-typescript.md § Result/EitherH
34Prefer Unions of Interfaces to Interfaces with Unionscovered11-design-patterns-typescript.md § discriminated unions; 03-typescript-type-system.mdH
35Prefer More Precise Alternatives to String Typescovered03-typescript-type-system.md § template-literal types; § branded types; 11-design-patterns-typescript.mdH
36Use a Distinct Type for Special Valuespartial11-design-patterns-typescript.md; 02-javascript-core.mdH
37Limit the Use of Optional Propertiespartial11-design-patterns-typescript.mdH
38Avoid Repeated Parameters of the Same Typepartial11-design-patterns-typescript.mdM
39Prefer Unifying Types to Modeling Differencespartial11-design-patterns-typescript.mdM
40Prefer Imprecise Types to Inaccurate TypesgapH
41Name Types Using the Language of Your Problem DomainskipL
42Avoid Types Based on Anecdotal Datapartial03-typescript-type-system.mdM
43Use the Narrowest Possible Scope for any Typescovered03-typescript-type-system.mdH
44Prefer More Precise Variants of any to Plain anycovered03-typescript-type-system.mdM
45Hide Unsafe Type Assertions in Well-Typed Functionscovered03-typescript-type-system.md § unsoundness; 05-data-structures-typescript.mdH
46Use unknown Instead of any for Values with an Unknown Typecovered03-typescript-type-system.md (46 mentions)H
47Prefer Type-Safe Approaches to Monkey Patchinggap03-typescript-type-system.md § module augmentation (mechanism only)M
48Avoid Soundness Trapscovered03-typescript-type-system.md § unsoundness; § array covariance is unsound; § method bivariance vs property contravarianceH
49Track Your Type Coverage to Prevent Regressions in Type SafetygapM
50Think of Generics as Functions Between Typescovered03-typescript-type-system.md § conditional types; § genericsH
51Avoid Unnecessary Type Parameterspartial03-typescript-type-system.mdM
52Prefer Conditional Types to Overload Signaturescovered03-typescript-type-system.md § distributive conditional typesH
53Know How to Control the Distribution of Unions over Conditional Typescovered03-typescript-type-system.md § distributive conditional types; § Equals<X,Y> via deferred conditionalsH
54Use Template Literal Types to Model DSLs and Relationships Between Stringspartial03-typescript-type-system.md § template-literal types; 02-javascript-core.mdH
55Write Tests for Your Typescovered03-typescript-type-system.md § 25 utility + 8 semantic assertions, compiler-verified; § Equals<X,Y>H
56Pay Attention to How Types Displaypartial03-typescript-type-system.mdM
57Prefer Tail-Recursive Generic Typescovered03-typescript-type-system.md § recursion depthM
58Consider Codegen as an Alternative to Complex Typespartial03-typescript-type-system.mdM
59Use Never Types to Perform Exhaustiveness Checkingcovered03-typescript-type-system.md § never (11 mentions); 11-design-patterns-typescript.md § discriminated-union state machinesH
60Know How to Iterate Over Objectspartial05-data-structures-typescript.md (Object.entries)H
61Use Record Types to Keep Values in Syncpartial03-typescript-type-system.md § Record; 11-design-patterns-typescript.md § satisfies-typed strategy mapsH
62Use Rest Parameters and Tuple Types to Model Variadic Functionscovered03-typescript-type-system.md § variadic tuplesM
63Use Optional Never Properties to Model Exclusive Orpartial03-typescript-type-system.md; 11-design-patterns-typescript.md § discriminated unionsH
64Consider Brands for Nominal Typingcovered03-typescript-type-system.md § branded/nominal types (12 mentions); 11-design-patterns-typescript.md (15)H
65Put TypeScript and @types in devDependenciesskipL
66Understand the Three Versions Involved in Type Declarationspartial03-typescript-type-system.mdM
67Export All Types That Appear in Public APIspartial11-design-patterns-typescript.mdM
68Use TSDoc for API CommentsskipL
69Provide a Type for this in Callbacks if It’s Part of Their APIcovered02-javascript-core.md § [[HomeObject]] and this-binding; 03-typescript-type-system.md § ThisTypeM
70Mirror Types to Sever Dependenciespartial11-design-patterns-typescript.mdM
71Use Module Augmentation to Improve Typescovered03-typescript-type-system.md § module augmentationM
72Prefer ECMAScript Features to TypeScript Featurescovered03-typescript-type-system.md § erasableSyntaxOnly; § TS 6.0/7.0; 16-testing-node-test.md § type stripping (the enum failure)H
73Use Source Maps to Debug TypeScriptgapM
74Know How to Reconstruct Types at Runtimepartial03-typescript-type-system.md (Zod mentioned); 19-testing-cheatsheet.mdH
75Understand the DOM Hierarchyskip03-typescript-type-system.md (two mentions)L
76Create an Accurate Model of Your Environmentpartial03-typescript-type-system.md; 02-javascript-core.md § CJS vs ESMM
77Understand the Relationship Between Type Checking and Unit Testingcovered16-testing-node-test.md; 18-testing-strategy.md § coverage is not verificationH
78Pay Attention to Compiler Performancepartial03-typescript-type-system.md § TS 7.0 (tsgo, ~10x)M
79Write Modern JavaScriptpartial02-javascript-core.md § ES2026 iterator helpers; § optional chaining/nullishM
80Use @ts-check and JSDoc to Experiment with TypeScriptpartial03-typescript-type-system.mdM
81Use allowJs to Mix TypeScript and JavaScriptpartial03-typescript-type-system.mdM
82Convert Module by Module Up Your Dependency Graphpartial02-javascript-core.md § module graphsM
83Don’t Consider Migration Complete Until You Enable noImplicitAnydiverges03-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:

optiondiagnostic on 6.0.3
--target es5TS5107: Option 'target=ES5' is deprecated and will stop functioning in TypeScript 7.0
--module amdTS5107
--moduleResolution node10TS5107
--downlevelIterationTS5101
--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:

typeas a set
never∅ — the empty set
'a' (literal)a singleton
'a' | 'b'set union
A & Bset intersection
unknownthe 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 narrowed ab to the literal 'a', so the assignment really was legal. declare is 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:

  • interface and type introduce only a type. const/let/function introduce only a value. class and enum introduce both, which is why they are the ones that confuse people.
  • typeof means 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 fixTS2551 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

ItemWhat to know
47 monkey patchingAttaching 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 coverageMeasure 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 mapsDebug 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 merginginterface only. Two interface Foo declarations merge; two type Foo are a duplicate-identifier error. This is what makes module augmentation (Item 71) work, and it is also an argument against interface for a closed type you do not want extended from elsewhere.

  • Unions, mapped types, conditional types, tuplestype only. An interface cannot be a union.

  • interface can extends (and gets a cached, cheaper assignability check); type composes 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 never
    i13_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'.

    extends refuses the conflict at the point of declaration. & accepts it silently and quietly makes that property never — which you discover much later, at the first assignment that cannot possibly succeed. The const probe: never line is the proof: it only compiles because i.x is genuinely never.

  • 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 erasableSyntaxOnly but 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/Either material 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. Use null/undefined or a distinct type. indexOf returning -1 is 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 (as clauses 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

  1. 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.
  2. §3 of this chapter before Item 2 or Item 83, so you do not learn the old flag story.
  3. The high-priority partials (§5): Items 1, 3, 33, 36, 37, 54, 60, 61, 63, 74.
  4. Skim chapters 5 and 6. Almost entirely covered by 03-typescript-type-system.md; dip in only where the matrix says partial.
  5. Chapters 8–10 are mostly partial or skip — 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