Overview
TypeScript 6.0 (March 2026) is the last release built on the JavaScript codebase, and TypeScript 7.0 (July 2026) is the native Go port, 8 to 12 times faster on full builds in Microsoft’s benchmarks. TypeScript 6 made strict: true the default. Install 7 with npm install -D typescript; it ships no programmatic API (7.1 is expected to add a new one), so tools that import the compiler use the @typescript/typescript6 compatibility package, and embedded-language editors (Vue, Svelte, Astro, MDX) cannot use 7 yet. Compiler options are in typescript-tsconfig; read general-principles first. For the head-to-head with JavaScript, see typescript-vs-javascript.
Keep strict on and add the flags it omits
Leave the strict family enabled and add noUncheckedIndexedAccess (makes arr[i] T | undefined), exactOptionalPropertyTypes, and noImplicitOverride. Turning off one strict flag to silence errors is a slow downgrade to untyped code. The flag-by-flag rules and a migration path are in typescript-strict-mode.
Use unknown, never any
any disables checking for everything it touches. Take unknown for values of unknown shape and narrow with a guard or a schema parser (typescript-narrowing). Put a legitimate any behind a named alias, and explain every @ts-expect-error on the same line; prefer it over @ts-ignore because it errors once the problem is fixed.
Parse at boundaries; never cast
as does not validate. Anything from fetch, JSON.parse, process.env, forms, or storage goes through a schema and the type is derived from it. See typescript-runtime-validation.
const Invoice = z.object({ id: z.uuid(), amountCents: z.number().int().nonnegative() })
type Invoice = z.infer<typeof Invoice>
const invoice = Invoice.parse(await res.json())Model states as discriminated unions
A tagged union lets the compiler narrow on the tag, which flat objects with optional fields cannot. End the switch with a never check so a new variant fails to compile.
type Invoice =
| { status: "draft"; id: string }
| { status: "paid"; id: string; paidAt: Date }Preserve literals with as const and check shapes with satisfies
as const keeps literal types. satisfies validates a value against a type without widening it, so you keep both the check and the specific type.
const ROUTES = { home: "/", docs: "/docs" } as const satisfies Record<string, `/${string}`>
type RoutePath = (typeof ROUTES)[keyof typeof ROUTES] // "/" | "/docs"Write ES modules
Set "type": "module" in package.json. Node can require() synchronous ES modules (no top-level await) since 22.12 and 20.19, which removes the main reason to dual-publish or write CommonJS. Under moduleResolution: "NodeNext", imports carry explicit extensions; under a bundler they do not.
Run .ts directly on Node only with erasable syntax
Node strips types by default since 22.18 and 23.6, and type stripping is stable from 24.12 and 25.2. It does no type checking, ignores tsconfig.json, requires file extensions in imports (./file.ts), refuses .ts files under node_modules, and rejects syntax that needs code generation: enums, runtime namespaces, and parameter properties. Set erasableSyntaxOnly: true so the compiler rejects those constructs, and run tsc --noEmit for type checking.