react-brain
Browse decisions
React & language foundations 3App architecture 11UI 13Platform & native 9Build, test, observe, secure 6AI in React apps 3stack composerdecision recordscensusstaleness benchnewsletterarchitecturechangelogmethodologyroadmap

entriesreact-foundations · verified 2026-09-24 · react + react-native

TypeScript rigor & public API typing

reviewedconfidence: mediumthis tier holds 71% on the public scorecard (7/20 graded · 0 overturned) →

related decisions: rn-versions · dx · build

cited by: rn-versions

re-verified 6× — 2026-09-24 · 2026-08-18 · 2026-07-28 · 2026-07-13 · 2026-07-10 · 2026-07-06 · changelog

recommendation

Run strict TypeScript (strict:true, the TS 6 default) on React and React Native alike, and export a small, deliberate public type surface from libraries. Move large repos to TypeScript 7 through the TS 6 bridge; what changes the timing is any tool that embeds the TS compiler API, which 7.0 does not ship.

  • library/package author → minimal, versioned exported types (no leaked internals)
  • large repo with slow tsc → TypeScript 7 via the TS 6 bridge (TS 6 and 7 side-by-side in CI, then 7 as default; the VS Code pattern in reading)
  • tool that imports the TS compiler API (typescript-eslint, custom build steps) → @typescript/typescript6 aliased as `typescript` beside TS 7 (keeps the 6.0 API and a tsc6 binary) until TS 7.1 ships its new API
  • Next.js repo moving to TS 7 → the tsc CLI checker (the `next build` default per the current docs, 16.3.6); leave experimental.useTypeScriptCli unset, because setting it to false makes the build exit under TS 7
  • Flow codebase outside Meta → TypeScript (a package-by-package exit with type integrity kept throughout; the Yelp case in reading)
  • React Native app on 0.87+ → Strict TypeScript API (the default: deep imports into react-native/Libraries/* are type errors; RB-E-RN-VERSIONS owns it)

Options & tradeoffs

the field considered — and why each one isn’t the default here

optiontradeoffevidence
strict mode tiersstrict vs gradual adoption for an existing codebase; the destination is fixed, because TS 6.0 made strict:true the default and TS 7.0 adopts 6.0's defaults (verified vs the TS 7.0 RC post).
exported type surfacewhat a library/package exports is its public API: keep it small, versioned, with no leaked internals. Heavy-generics libraries can cut type-instantiation cost 62-86% with named techniques (see reading: TypeScript Performance in TanStack Table V9).
TypeScript 7 (native/Go)Go rewrite of tsc: ~10x faster than TS 6.0, 10.6x–16.7x on five large codebases with --checkers 8; makes every 6.0 deprecation a hard error and ships no compiler API yet. 7.0.2 (2026-07-08, verified vs npm 2026-09-24); see note: TOOLING GAP.365.3M/wk · ships in 28/34
Flow (2026)Meta's type checker; syntax converged with TS and the compiler was ported to Rust, but it is mostly Meta-internal now, so it is not a pick outside Meta (see the Flow when-clause).

evidence: npm weekly downloads (signals snapshot) · “ships in n/D” = adoption across the production-app census, honest denominators

Migration lines

version and deprecation lines this decision tracks — for YOUR repo's sequenced plan: the doctor or npx -y @heart-it/react-brain migrate .

typescript < 7.0.0 → TypeScript 7 (Go-native tsc) via the TS 6 bridge [upgrade · effort M]

TS 7.0 is STABLE (~10x, up to 16x type-checking with parallelization); migrate incrementally — TS 6 as the low-churn bridge, TS 6 and 7 side-by-side in CI, then TS 7 as default (the VS Code ~50-extension migration pattern; their blocker was formatter diffs, not type errors). Check tools that EMBED the TS compiler API first — TS 7 doesn't expose it yet, e.g. `next build` needs experimental.useTypeScriptCli to shell out to the tsc CLI instead

registry.npmjs.org/typescript/latest · code.visualstudio.com/blogs/2026/06/26/iterating-faster-… · nextjs.org/docs/app/api-reference/config/next…

npm weekly downloads (from the corpus's last signals run): typescript 365.3M

Verified notes

Strictness is a boundary discipline: the checker earns its keep where data crosses an edge, which is exactly where teams switch it off with `any`. strict:true is now the default rather than a stance, so the open questions are how an existing codebase gets there and how small the exported type surface stays. The compiler itself changed engines: TypeScript 7 is the Go-native tsc, and the only thing that decides when you move is whether your tooling needs the compiler API that 7.0 does not yet ship. TS 7 STABLE (2026-07-08, verified vs npm + the TS 7.0 announcement): npm latest 7.0.2. About 10x faster than TS 6.0 at the default --checkers 4; with --checkers 8 the post measures 10.6x (tldraw) to 16.7x (VS Code) across five codebases, at the cost of more memory. 7.0 adopts 6.0's defaults and turns every 6.0 deprecation into a hard error → clear deprecations on TS 6 first, then run 6 and 7 side-by-side in CI (the VS Code case in reading: type-checking 36s→5s). TOOLING GAP (2026-09-24, verified vs the TS 7.0 announcement + the Next.js TypeScript docs): 7.0 ships no compiler API; the TypeScript team expects 7.1 to ship a new, different one. Until then, tools that import `typescript` (typescript-eslint is the named example) stay on 6.0 through @typescript/typescript6: alias `typescript` to it (it provides a tsc6 binary and re-exports the 6.0 API) and add a second alias for 7.0's tsc. Next.js closed its own gap: current docs (16.3.6) say `next build` runs the project-local tsc CLI by default, so TS 7 needs no config, and setting experimental.useTypeScriptCli to false makes the build exit under TS 7. Trade-offs of the CLI path: the flag is still labelled experimental, diagnostics lose Next.js code frames, and the check covers the whole tsconfig project, tests included → audit every other API-embedding tool before pinning typescript@7. FLOW EXIT (verified vs the Yelp post): outside Meta the answer is TypeScript, and the exit is a program, not a codemod — convert deepest dependencies first and keep type-checking on everywhere. The reading "Migrating a Large Flow Monorepo to TypeScript" holds the mechanism and numbers. RB-E-RN-VERSIONS owns the RN 0.87 Strict TypeScript API default; RB-E-DX owns the CI typecheck gate; RB-E-BUILD owns emit vs check. HISTORY: 2026-09-24 CORRECTED: Next.js no longer needs experimental.useTypeScriptCli set (the tsc CLI checker is the `next build` default); "up to 16x" pinned to the 7.0 post's 16.7x with --checkers 8; @typescript/typescript6 added as the API bridge. 2026-08-18 GAP CLOSED: Flow→TS exit case (Yelp, 1.4M SLoC over 3y7m) + RN 0.87 Strict-API cross-ref. 2026-07-28 GAP CLOSED: TS 7 tooling gap — `next build` needed experimental.useTypeScriptCli (16.3 Preview).

Canonical reading

Editorial annotations on why each piece matters — the articles themselves are the originals; read them there.

Migrating a Large Flow Monorepo to TypeScriptShawn Walton (Yelp Engineering)

The receipt under this entry's 'not at Meta → use TypeScript, not Flow' line, and the shape of a type-checker migration you cannot do in one cut: 1.4M SLoC across 570 packages, three years and seven months, ending February 2026. The load-bearing decision was refusing to suspend type-checking anywhere — 'code that loses coverage from its type checker, tests, or linter almost never regains it' — which ruled out a big-bang rewrite and forced Flow and TypeScript to coexist in production. The mechanism: convert one package at a time (flowts first, later Stripe's flow-to-typescript-codemod, which cut the manual tail), then run flowgen to turn the emitted .d.ts back into Flow headers so still-Flow consumers keep type-checking. Because no tool generates .d.ts FROM Flow, the order is forced — deepest internal dependencies first, up the tree. What actually moved the needle was not tooling: a year of dogfooding on ~90 foundational packages before launch, documentation and outreach, a dependency-graph score that ranked which package unblocks the most others, auto-filed Jira tickets when a package became convertible, and finally hard quarterly deadlines — voluntary migration plateaued once the enthusiasts were done. LLM-driven conversion was tried in 2023 and abandoned as unreliable. Outcome: type coverage 83.15% (Flow) → 96.44% (TS), ~6,000 @ts-expect-error (2,000 of them mass-added when the team auto-suppressed the last few dozen orphaned packages to finish), 82% of surveyed engineers reporting a productivity gain.

Why does tsgo use so much memory?Zackary Radisic

A heap-level investigation of the Go TypeScript compiler — per-thread Checker duplication, never-freed types, symbol duplication, AST at 45% of heap, plus a fix. A durable deep-dive on TS7/tsgo internals and its concurrency model's cost.

TypeScript Performance in TanStack Table V9Kevin Van Cott (TanStack)

Concrete techniques to cut type-instantiation cost 62-86% (feature maps, materialized interfaces, in/out variance annotations, explicit type args), with measurements. The reference for heavy-generics library authors.

Iterating Faster with TypeScript 7VS Code team (Microsoft)

The production adoption case the tsgo-internals piece lacks: VS Code's ~50-extension codebase cut type-checking 36s→5s (~7x) and editor project-load 60s→10s by migrating INCREMENTALLY — TS 6.0 as a low-churn bridge, then TS 6-and-7 side-by-side in CI (TS 6 still emitting), then esbuild + TS 7 as the default. The surprising blocker wasn't type errors but formatter differences failing pre-commit checks. Evidence that TS 7 (Go-native tsc) is trustworthy for daily use on a large real codebase — if you bridge through TS 6 first.

Sources

Depth (in-domain rules) is owned by the engineering-principles skill — this entry is selection breadth.

The full explanation

The reviewed long-form essay behind this entry — the why, not a how-to. Also on GitHub.

About TypeScript rigor & public API typing

Diataxis: Explanation. This page builds understanding of how much TypeScript strictness to buy and where — the reasoning behind the picks. It is not a tsconfig how-to or a migration runbook. API-design and module-boundary depth is owned by engineering-principles; the CI typecheck gate by RB-E-DX; emit and bundling by RB-E-BUILD. Read this for the why.

The one principle that organises everything: strictness is a boundary discipline

The checker earns its keep where data crosses a boundary — and that's exactly where teams tend to switch it off. The entry's own smell detector says it plainly: explicit any showing up across many files means "the checker is off exactly where bugs concentrate — prefer unknown + narrowing at boundaries." The same principle flips outward for library authors: the exported type surface is itself a boundary — something to minimize and version deliberately, not a side effect of whatever happens to be exported. Strictness inside, a small deliberate surface outside: one discipline, two directions.

The default, and why

Run strict TypeScript (strict:true, the TS 6 default) and export a small, deliberate public type surface from libraries.

strict:true stopped being a stance and became the baseline: TS 6.0 makes it the default, so running loose is now an opt-out you have to justify, not a starting point. The "strict vs gradual adoption" tradeoff still exists for legacy code, but the destination is fixed. And the library half of the default is the boundary principle applied to your consumers: if you're a library/package author, minimize and version the exported types, and avoid leaking internals — your .d.ts is a public API with all the compatibility obligations that implies.

The landscape, and when each piece earns its place

Strict mode tiers — the strict-vs-gradual adoption axis. With strict:true as the TS 6 default, "tiers" is about how an existing codebase gets there, not whether to go.

Exported type surface — what a library/package should export. Minimal, versioned, no leaked internals. For heavy-generics libraries, the TanStack Table V9 work is the reference: concrete techniques (feature maps, materialized interfaces, in/out variance annotations, explicit type args) cut type-instantiation cost 62–86%, with measurements.

TypeScript 7 (native/Go) — the Go rewrite of tsc. 7.0 is STABLE: npm latest is 7.0.2, published 2026-07-08 (verified against the npm registry; the RC was 2026-06-18, and stable landed on schedule). ~10x faster than TS 6.0 at the default --checkers 4; with --checkers 8 the 7.0 announcement measures 10.6x–16.7x across five large codebases, at more memory. It turns every 6.0 deprecation into a hard error. Earns its place the moment a large repo has a slow tsc.

The tooling gap (verified 2026-09-24 vs the TS 7.0 announcement and the Next.js docs). 7.0 ships no compiler API; the TypeScript team expects 7.1 to ship a new, different one. Tools that import typescript (typescript-eslint is the named example) stay on 6.0 through @typescript/typescript6, aliased as typescript beside a second alias for 7.0's tsc. Next.js no longer needs a flag: its current docs (16.3.6) say next build runs the project-local tsc CLI by default, and setting experimental.useTypeScriptCli to false makes the build exit under TS 7. So the question that sets your TS 7 date is not speed but which of your tools embed the API.

TypeScript 6.0 (the bridge) — the JS-based release you migrate through, not to. The VS Code case study is the pattern: their ~50-extension codebase cut type-checking 36s→5s (~7x) and editor project-load 60s→10s by going incrementally — TS 6.0 as a low-churn bridge, then TS 6 and 7 side-by-side in CI (TS 6 still emitting), then esbuild + TS 7 as the default.

Flow (2026) — Meta's type checker. Syntax has converged with TS and the compiler was ported to Rust, but it's mostly Meta-internal now. The when-clause is one line: not at Meta → use TypeScript, not Flow. Yelp's exit (1.4M SLoC, three years and seven months) shows the shape: a package-by-package program, deepest dependencies first, with type-checking never suspended.

React Native 0.87+ makes the Strict TypeScript API the default, so deep imports into react-native/Libraries/* become type errors; RB-E-RN-VERSIONS owns that migration.

Tradeoffs and failure modes to name out loud

  • any at the boundary. The entry flags explicit any in 10+ files as a repo-level smell: it turns the checker off exactly where bugs concentrate. unknown + narrowing is the strict-era replacement.
  • A leaky exported surface. Exporting whatever's convenient makes internals part of your public API; the when-clause for library authors is minimize + version, avoid leaking internals.
  • Generics-heavy types that melt the checker. Type-instantiation cost is real and measurable — TanStack Table V9's 62–86% reductions show it's also fixable, with named techniques.
  • Big-bang TS 7 migration. The VS Code evidence says bridge through TS 6 and run 6-and-7 side-by-side in CI first. Their surprising blocker wasn't type errors at all — it was formatter differences failing pre-commit checks.
  • Deprecated-flag debt. TS 7 removes long-deprecated flags — the TS 6 bridge exists to flush those out of your config first.
  • The native compiler's memory bill. The tsgo heap investigation documents the concurrency model's cost — per-thread Checker duplication, never-freed types, symbol duplication, AST at 45% of heap (plus a fix). Speed came from parallelism, and parallelism has a footprint.

How it interacts with the rest of the stack

  • DX (RB-E-DX). Typecheck is one of the invariants CI must gate — an ungated strict:true is a suggestion. Fittingly, VS Code's TS 7 blocker lived in this loop too: formatter diffs failing pre-commit checks, not type errors.
  • Build (RB-E-BUILD). The migration pattern separates checking from emitting: TS 6 kept emitting while TS 7 checked side-by-side, and the end state was esbuild + TS 7 as the default.
  • API/boundary depth (engineering-principles). What a package should export, how boundaries are drawn, and dependency direction are owned there; this entry supplies the type-level rule — small, versioned, deliberate.
  • Upgrades. react-brain migrate flags any typescript below 7.0.0: upgrade via the TS 6 bridge, effort M, following the VS Code pattern above.

In one paragraph

TypeScript rigor is a boundary discipline: run strict:true (the TS 6 default) so the checker is on where data crosses edges — any in bulk means it's off exactly where bugs concentrate, so prefer unknown + narrowing — and, as a library author, treat the exported type surface as a boundary too: small, versioned, no leaked internals. Meanwhile the compiler changed engines: TS 7 (Go-native tsc) is stable — 7.0.2 on npm as of 2026-07-08, ~10x faster, 16.7x at best with --checkers 8 — and the proven adoption path is incremental: TS 6.0 as the low-churn bridge, 6 and 7 side-by-side in CI, then TS 7 as default, the way VS Code cut type-checking 36s→5s. Not at Meta? TypeScript, not Flow.


See also: RB-E-DX (CI gates typecheck), RB-E-BUILD (emit vs check — esbuild + TS 7 end state). API and boundary depth: the engineering-principles skill.

Related in react-foundations: react-core · rn-versions