Case file — T2

No Escape Hatches

The type system isn't optional.

Every any, every unchecked unknown, every as cast and every @ts-ignore is a hole in the contract. The compiler stops checking; the bug waits for production to find it.

ByAdam LewisPublished3 May 2026Reading11 minVersionv1.0ConfidenceHigh
§0b

Opinion

I've watched this fight in three different codebases and the verdict is always the same: one any in March is forty by September. The escape hatch is not the bug; the escape hatch is the shape of the bug, the place where the next reader cannot tell if anyone ever did the work. any says nothing; as Foo says the author hoped it was a Foo; @ts-ignore says the author gave up. None of those are checked, none of those refactor with the rest of the code, and none of those tell the reviewer where the load-bearing assumption lives.

The rule has narrow exceptions, and I'll defend them as part of the rule, not against it. Some test setups need any to mock framework internals or boundaries that the test itself is the contract for. Some third-party shapes (legacy SDKs without typings, free-form JSON from an unowned upstream) cannot honestly be modelled. At the mocked edge between two systems, where the shape is the test's concern, the escape hatch is the right tool. Everywhere else it is technical debt with a friendly face. See T1 Domain-Driven Types: the escape hatch is the negative form of the same rule. T1 says encode the domain. T2 says do not bypass the encoding.

Copy a note and link

Grab this short comment and drop it into a PR comment or an LLM chat to prompt the right change.

No `any`, no unchecked `unknown`, no `as` casts (except `as const`), no `@ts-ignore`. Narrow exceptions belong in the commit message: test mocks, third-party shapes that cannot be modelled, mocked-boundary edges. Anywhere else, type the parameter or write the guard.

/tenet/no-escape-hatches/T2
§0c

AI eyes only

Rule: the type system is not optional. No any, no unchecked casts.

Reject: any, unknown cast directly to a domain type, as without a runtime check, @ts-ignore, @ts-expect-error without a cited bug or upstream issue.

Generate: type the parameter explicitly. Write the type guard. Model third-party shapes with unknown plus a runtime schema (Zod / Valibot) before reaching the domain.

Diagnostic: search the diff for any, as , @ts-ignore. Each must be removed or justified inline against a referenced issue.

§0d

Why?

  • Keeps the compiler doing the work it ships to do. Every any turns the type checker off for the values it touches; banning the escape hatch keeps the proof intact.
  • Forces honesty at the boundary. JSON.parse, localStorage, postMessage and route input land as unknown and stay that way until a guard from T3 Type Guards says otherwise.
  • Refactors stay safe. any hides the shape; rename a field on a real type and the compiler walks the call graph for you.
  • Coding agents can't cheat the type system. function process(data: any): any is the model's favourite shortcut; the linter set to error makes the agent type the parameter instead.
  • Cuts review time. The shape of every value is in the signature; the reviewer reads the type, not the implementation, to know what came in.
  • The narrow-exceptions list earns its keep. Test mocks for framework internals, third-party shapes you genuinely cannot model, and mocked-boundary edges get a written justification on the line above; everything else gets the refactor.
  • @ts-expect-error rots out on its own. The day the underlying type bug is fixed, the directive errors and gets removed; @ts-ignore would have stayed there forever.
The receipts
Origins, quoted passages, evidence, the strongest counter-argument and the reply.
§1

Origins

The argument is older than TypeScript and broader than any one language. Tony Hoare called his 1965 invention of the null reference his “billion-dollar mistake” for exactly this reason: a type that admitted the bad case shipped the bad case every time, and the escape hatch — the silent null — cost the industry a billion dollars in production failures.5C. A. R. HoareNull References: The Billion Dollar Mistake (QCon, 2009). Hoare named his 1965 invention of the null reference the canonical type-system escape hatch and totalled the industry cost. Benjamin Pierce's Types and Programming Languages (2002) gave the academic statement: a type system is a syntactic method for proving the absence of certain program behaviours. any withdraws the proof.6Benjamin C. PierceTypes and Programming Languages (MIT Press, 2002), ch. 1. The academic statement: a type system is a tractable syntactic method for proving the absence of certain program behaviors.

In the TypeScript-specific lineage, the inflection point is Anders Hejlsberg's 2018 TSConf keynote announcing TypeScript 3.0 and unknown. Hejlsberg framed the new type as “the safe any” — explicit acknowledgement that the team had been losing the fight against the original escape hatch and now needed a typed replacement that forced narrowing before use.2Anders HejlsbergTypeScript 3.0 release notes (Microsoft Developer Blogs, 2018). Introduces unknown as the safe any: explicit acknowledgement that the team had been losing the fight against the original escape hatch. Every TypeScript release since has tightened the rule from the language side: noUncheckedIndexedAccess (4.1), exactOptionalPropertyTypes (4.4), verbatimModuleSyntax (5.0). The language is doing the work of saying: don't silence me.

The tooling tradition is older still. Yaron Minsky's 2011 Effective ML talk argued that the discipline of “make illegal states unrepresentable” included “make absent values explicit” — the option-type pattern that ML, F#, Haskell, Scala and Rust have used for decades to refuse the null escape hatch.7Yaron MinskyEffective ML / Make Illegal States Unrepresentable (Jane Street, 2011). The ML/OCaml form of the discipline; the option type as the structural refusal of the null escape hatch. Dan Vanderkam's Effective TypeScript Item 38 codified the industrial form for the TS audience: limit the use of any to the narrowest possible scope, never let it leak out of a function.8Dan VanderkamEffective TypeScript (O’Reilly, 2019), Item 38. Limit any to the narrowest possible scope and never let it leak out of the function it lives in. The rule is not zero tolerance; it is zero silent tolerance.

Robert C. Martin would call the escape hatch a violation of the contract.9Robert C. MartinClean Code (Prentice Hall, 2008). The escape hatch is a violation of the contract: the type promised something the runtime did not enforce. Joshua Bloch in Effective Java Item 16 frames the same discipline at the class boundary: hide the implementation; never let a caller depend on the absence of a check. F6 Encapsulation is the structural form of the rule; T2 is the type-system form. Both say the same thing: the contract is the promise; if you can break the promise without an explanation, the promise is a lie.

§2

Quotes

unknown is the type-safe counterpart of any. Anything is assignable to unknown, but unknown isn't assignable to anything but itself and any without a type assertion or a control-flow-based narrowing.

Anders Hejlsberg · TypeScript 3.0 release notes (2018)

A type system is a tractable syntactic method for proving the absence of certain program behaviors by classifying phrases according to the kinds of values they compute.

Benjamin C. Pierce · Types and Programming Languages (2002), ch. 1

I call it my billion-dollar mistake. It was the invention of the null reference in 1965. My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler.

C. A. R. Hoare · Null References: The Billion Dollar Mistake (QCon, 2009)

Use the narrowest possible scope for any types. Never return an any type from a function. This will silently lead to the loss of type safety for any client code that calls the function.

Dan Vanderkam · Effective TypeScript (2019), Item 38
§3

Evidence

Twenty external sources, ranked by author authority. The first five are the canon; expand to see the rest, including the qualifiers and the named opposers. Each links out to its primary source.

  1. 01
    Anders Hejlsberg · 2018
    The team shipping the language framed unknown as the safe any: explicit acknowledgement that any was the escape hatch they had been losing the fight against.
  2. 02
    Dan Vanderkam · 2019
    Vanderkam codifies the industrial form of the rule: the any type is sometimes necessary, but it must never leak past the narrowest scope you can manage.
  3. 03
    Dan Vanderkam · 2019
    The book-length statement of the rule. Item 5 is the canonical reference cited by every TypeScript style guide downstream.
  4. 04
    TypeScript team · 2020+
    The handbook itself names any as an opt-out of the type checker and recommends turning on noImplicitAny. The qualifier: the language ships with the escape hatch and tells you not to use it.
  5. 05
    TypeScript team · 2020+
    Handbook section that distinguishes unknown from any: unknown forces narrowing before use, which is the discipline T2 demands at the boundary.

Sixteen sources, one direction with caveats. The supports are the canon: Hejlsberg's release notes, Vanderkam's Effective TypeScript, and the TypeScript Handbook pages on any and unknown. The qualifiers further down carry the narrow-exception clause the rule has to earn. The opposers (gradual-typing apologists, dynamic-language counters) are mostly arguing for a smaller version of the rule, not against it.

§4

Examples

Viewing: TypeScript.
Avoid
Fileload-hedgehog.ts
// Before: any in, as out. The compiler stops checking at the boundary.function loadHedgehog(input: any): Hedgehog {  const payload = JSON.parse(input);  return payload as Hedgehog;}
Prefer
Fileload-hedgehog.ts
// After: unknown in, guard validates, one logged exception for the SDK.function loadHedgehog(input: string): Hedgehog {  const payload: unknown = JSON.parse(input);  if (!isHedgehog(payload)) {    throw new Error("invalid hedgehog payload");  }  return payload;}// allowed exception: legacy SDK ships no typings; ticket HEDGE-412 to retire.import burrowSdk from "burrow-legacy-sdk" as any;
§4b

Enforcement

Viewing: TypeScript.

Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.

RuleToolCatches
@typescript-eslint/no-explicit-anytypescript-eslintany explicit any annotation. The narrow exceptions live in eslint overrides for test files and vendored .d.ts shims.
@typescript-eslint/no-unsafe-argumenttypescript-eslintan any (or unknown) value flowing into a typed parameter without narrowing — closes the laundering route through function calls.
@typescript-eslint/no-unsafe-assignmenttypescript-eslintassignment of an any value to a typed variable — the way most any leaks start.
@typescript-eslint/no-non-null-assertiontypescript-eslintthe non-null assertion (!) — the same crime as as with extra steps. Forces a guard or a refactor instead.
@typescript-eslint/consistent-type-assertionstypescript-eslintas casts on object literals (the silent any-bypass for fresh values) and inconsistent assertion styles.
@typescript-eslint/ban-ts-commenttypescript-eslint@ts-ignore, @ts-nocheck, and undocumented @ts-expect-error. Forces every suppression to carry a justification of at least 10 characters.
tsconfig strictTypeScript compilerimplicit any from un-annotated parameters, mis-typed null/undefined flows, and the rest of the strict family. The single setting that turns on most of the language’s safety net.
tsconfig noUncheckedIndexedAccessTypeScript compilerindexed access into a record without a presence check — the silent undefined that becomes a runtime crash.
eslint.config.mjsconfiguration snippet
import tseslint from 'typescript-eslint';

export default tseslint.config({
  files: ['**/*.{ts,tsx}'],
  languageOptions: {
    parserOptions: { project: './tsconfig.json' },
  },
  rules: {
    '@typescript-eslint/no-explicit-any': 'error',
    '@typescript-eslint/no-unsafe-argument': 'error',
    '@typescript-eslint/no-unsafe-assignment': 'error',
    '@typescript-eslint/no-unsafe-call': 'error',
    '@typescript-eslint/no-unsafe-member-access': 'error',
    '@typescript-eslint/no-unsafe-return': 'error',
    '@typescript-eslint/no-non-null-assertion': 'error',
    '@typescript-eslint/consistent-type-assertions': ['error', {
      assertionStyle: 'as',
      objectLiteralTypeAssertions: 'never',
    }],
    '@typescript-eslint/ban-ts-comment': ['error', {
      'ts-ignore': true,
      'ts-expect-error': 'allow-with-description',
      'ts-nocheck': true,
      minimumDescriptionLength: 10,
    }],
  }
});
§4c

AI rules

File.cursor/rules/t2-no-escape-hatches.mdc
---
description: Prickles T2 — No Escape Hatches
globs: "**/*.{ts,tsx,js,jsx,py,java,php}"
alwaysApply: false
---

## Prickles T2 — No Escape Hatches

Forbid the four escape hatches by default: `any`, `unknown` without narrowing, `as` casts on anything other than `as const`, and `@ts-ignore` / `@ts-expect-error`.

Narrow exceptions, written down in the commit message, not in a code comment: test setups that mock framework internals or system boundaries; legacy third-party shapes that genuinely cannot be modelled; mocked edges where the test is the contract.

Where `unknown` enters the system from outside the type graph (JSON parsing, `localStorage`, `postMessage`, route input, error catch), narrow with a guard before the value flows further.

Prefer `@ts-expect-error` over `@ts-ignore`. The former errors when the underlying bug is fixed; the latter rots silently.

Refuse to satisfy a type checker by silencing it. If the linter trips, fix the type, write the guard, or model the boundary.

Repo layout, CI, and ESLint wiring for these paths live on /implementation — not repeated on every tenet.

§5

Counter-argument

Counter

Hejlsberg himself is the steelman. At TypeScript's 2012 launch he was explicit that TS is a gradual type system and any is a feature: the bridge that lets a JavaScript codebase migrate one file at a time.1Anders HejlsbergTypeScript launch keynote (Channel 9, 2012). Hejlsberg framed TypeScript as a gradual type system: any is the bridge that lets a JavaScript codebase migrate one file at a time. The steelman for the rule and the bridge metaphor that the reply takes seriously. Take that seriously and the rule looks dogmatic: it bans the feature the language ships with. The dynamic-language counter goes further: types ossify a design before the design is known, and the cost of changing a pervasive type is steeper than the cost of a runtime adjustment. Sandi Metz's argument from The Wrong Abstraction applies here too: the wrong type is harder to remove than the wrong cast.

§6

Counter-argument retort

Reply

Hejlsberg's gradual-typing argument was right at the time of writing and is wrong as a doctrine for the codebase that lives there now. Gradual typing was the bridge; the bridge has a far end. Once a project is past the migration, the escape hatch stops being a feature and starts being technical debt with a friendlier face.1Anders HejlsbergTypeScript launch keynote (Channel 9, 2012). Hejlsberg framed TypeScript as a gradual type system: any is the bridge that lets a JavaScript codebase migrate one file at a time. The steelman for the rule and the bridge metaphor that the reply takes seriously. The TypeScript team itself agrees by action: the 3.0 release introduced unknown as “the safe any” precisely because the team had been losing the fight, and every release since has tightened the screws — strict defaults, noUncheckedIndexedAccess, exactOptionalPropertyTypes, verbatimModuleSyntax.2Anders HejlsbergTypeScript 3.0 release notes (Microsoft Developer Blogs, 2018). Introduces unknown as the safe any: explicit acknowledgement that the team had been losing the fight against the original escape hatch. The language is voting against its own escape hatch.

The Metz objection — the wrong type is harder to remove than the wrong cast — is sharper, and it's the reason the rule has narrow exceptions instead of being absolute. A type-driven design can be wrong; a Zod schema for an external API can be over-specified; a branded primitive can outlive its usefulness. The exceptions list isn't an apology, it's part of the principle: at boundaries you don't own, model what you can and let unknown + a guard carry the rest. See T3 Type Guards for the runtime side.

The genuine residue is the four shapes the rule names. Each gets a default and a carve-out written down in the commit, not as a vibe. any: forbidden, except in test setups that mock framework internals and in shims for legacy modules awaiting a .d.ts. unknown: forbidden without narrowing, except at the system boundary where JSON.parse and localStorage and postMessage hand you a shape you have to validate downstream.3TypeScript HandbookThe unknown type. unknown is the principled alternative to any: it forces narrowing before use, which is the discipline T2 demands at the system boundary. as: forbidden on anything except as const, with the genuinely-witness-pattern carve-out for runtime invariants established elsewhere. @ts-ignore: forbidden in favour of @ts-expect-error, which errors when the underlying bug is fixed and the suppression is no longer needed.4typescript-eslint maintainersban-ts-comment rule. Polices @ts-ignore, @ts-expect-error and @ts-nocheck. Prefer @ts-expect-error: it errors when the underlying bug is fixed and the suppression is no longer needed.

In production code the escape hatch is the comment that lies. The next reader trusts the type they see; the type was a cast; a bug ships. Refactor — type the parameter, write the guard, model the boundary, file the ticket. The discipline is small. The cost of pretending you can skip it is large.

§7

Notes

  1. [1]Anders HejlsbergTypeScript launch keynote (Channel 9, 2012). Hejlsberg framed TypeScript as a gradual type system: any is the bridge that lets a JavaScript codebase migrate one file at a time. The steelman for the rule and the bridge metaphor that the reply takes seriously.
  2. [2]Anders HejlsbergTypeScript 3.0 release notes (Microsoft Developer Blogs, 2018). Introduces unknown as the safe any: explicit acknowledgement that the team had been losing the fight against the original escape hatch.
  3. [3]TypeScript HandbookThe unknown type. unknown is the principled alternative to any: it forces narrowing before use, which is the discipline T2 demands at the system boundary.
Disagree? Found a hole in the argument? Take issue with this tenet →
Last revised: 2026-04-27