--- title: "TypeScript Best Practices (2026)" description: "Agent-facing 2026 TypeScript guidance: verified TS 6.0/7.0 release timeline (Go-native compiler RC), the excess-property-check structural-typing footgun with its exact trigger conditions, and strict-mode flags worth enabling beyond bare `strict: true`." language: typescript framework: null category: language_best_practices tags: - best-practices - typescript - strict-mode - type-safety - discriminated-unions - structural-typing - project-references keywords: - typescript 6.0 final javascript release march 2026 - typescript 7.0 go native compiler release candidate - excess property check footgun typescript - noUncheckedIndexedAccess exactOptionalPropertyTypes - typescript project references monorepo last_updated: "2026-07-08" difficulty: intermediate version: "TypeScript 6.0 (final JS-based release, shipped 2026-03-23), TypeScript 7.0 (Go-native, RC 2026-06-18)" related: - ../README.md - ../code_style_guide.md - ../frameworks/frontend/react_best_practices.md - ../frameworks/backend/nodejs_best_practices.md - ../../python/frameworks/fastapi/best_practices.md - ../../../core/principles/solid.md - ../../../core/architecture/clean_architecture.md search_priority: high status: published --- # TypeScript Best Practices (2026) Assumes fluency with basic TypeScript syntax. No type-system primer. ## Release timeline - verified, don't guess a version number | Release | Status (2026-07) | Detail | |---|---|---| | TypeScript 5.9 | Superseded | Bridge release preceding 6.0 | | TypeScript 6.0 | **Shipped 2026-03-23** | Final JavaScript-based release; deprecates features to align with 7.0; type-checking behavior kept highly compatible with 7.0 | | TypeScript 7.0 ("Project Corsa") | **Release Candidate as of 2026-06-18** | Full compiler port to Go; GA expected roughly one month after RC (i.e., mid-to-late July 2026 - re-verify before citing as shipped); ~10x faster type-checking than 6.0; in 19,926 of 20,000 internal compiler test cases produces the same diagnostics as 6.0 (74 known divergences) | Practical implication: a codebase on 6.0 today has a low-risk upgrade path to 7.0 once GA - the type-checking surface is deliberately near-identical. Do not assume 7.0 requires a rewrite; assume it requires a recompile and a spot-check of the 74 known-divergent cases if they matter to the codebase. ## The excess-property-check footgun - a real, frequently-misunderstood inconsistency TypeScript's structural typing means two independently-declared types with identical shape are interchangeable - except at **fresh object-literal boundaries**, where TypeScript applies a stricter check that does not follow from structural typing alone: ```ts interface Config { mode: "aggressive" | "safe" } function apply(c: Config) {} apply({ mode: "safe", retries: 3 }) // Error: Object literal may only specify known properties, // and 'retries' does not exist in type 'Config'. const draft = { mode: "safe", retries: 3 } apply(draft) // No error - draft is a variable, not a fresh literal, so excess-property // checking does not apply, even though draft's type is structurally // assignable to Config either way. ``` This is not a bug - excess-property checking exists specifically to catch typos like `colour` vs `color` at the one point structural typing would otherwise let them through silently - but the trigger condition (fresh literal only, direct assignment or argument position) is the actual footgun: the exact same object shape is accepted or rejected purely based on whether it passed through an intermediate variable first. Real-world impact: refactoring `apply({ ...literal })` into `const cfg = {...}; apply(cfg)` silently disables a typo check the team was relying on. Don't rely on excess-property checking as a substitute for a runtime schema validator (Best Practice below) - it is a literal-shape lint, not a type-safety guarantee. ## Strict mode - the flags beyond bare `strict: true` worth calling out ```jsonc { "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true, "noImplicitOverride": true, "noFallthroughCasesInSwitch": true, "module": "NodeNext", "moduleResolution": "NodeNext", "isolatedModules": true, "skipLibCheck": true } } ``` - `noUncheckedIndexedAccess`: without it, `arr[i]` is typed `T`, not `T | undefined` - TypeScript silently assumes every index access succeeds. This is not covered by plain `strict: true`. - `exactOptionalPropertyTypes`: without it, `{ email?: string }` treats "key absent" and "key present with value `undefined`" as the same type, which is not true at runtime for `JSON.stringify`/structural equality checks - a frequent source of API drift bugs invisible to the type checker. ## Runtime validation at boundaries - non-negotiable, type checking is compile-time only ```ts import { z } from "zod" const UserSchema = z.object({ id: z.string().uuid(), email: z.string().email(), role: z.enum(["admin", "member"]), }) type User = z.infer async function fetchUser(id: string): Promise { const response: unknown = await fetch(`/api/users/${id}`).then((r) => r.json()) return UserSchema.parse(response) } ``` `unknown`, not `any`, at every boundary where data enters from outside the process (HTTP bodies, `JSON.parse`, env vars, third-party libraries without their own types) - `any` is contagious and disables checking for everything it touches downstream. ## Discriminated unions over optional-field bags - unchanged, still the highest-leverage modeling pattern ```ts type FetchState = | { status: "idle" } | { status: "loading" } | { status: "success"; data: T } | { status: "error"; error: Error } ``` Makes `{ status: "success", error: someError }` unrepresentable instead of merely unlikely. ## `as` casts vs type guards vs `@ts-expect-error` | Pattern | Runtime guarantee | Verdict | |---|---|---| | `value as User` | None | Replace with a type guard or schema validation | | `function isUser(v: unknown): v is User { ... }` | Real, checked at the call site | Preferred | | `@ts-ignore` / `@ts-nocheck` | None, permanently silences the checker at that line | Forbidden - the underlying error is never resolved | | `@ts-expect-error` | None, but **fails the build once the underlying error is fixed** | Acceptable as a tracked, self-expiring suppression; require a comment explaining why | ## Sources - [TypeScript 6.0 Ships as Final JavaScript-Based Release - Visual Studio Magazine](https://visualstudiomagazine.com/articles/2026/03/23/typescript-6-0-ships-as-final-javascript-based-release-clears-path-for-go-native-7-0.aspx) - [Go-based TypeScript 7.0 reaches release candidate stage - InfoWorld](https://www.infoworld.com/article/4191918/typescript-7-0-reaches-release-candidate-stage.html) - [TypeScript 7.0 RC: The Go-Native Compiler Has Landed](https://www.digitalapplied.com/blog/typescript-7-0-rc-go-native-compiler-2026-upgrade-guide) - [Be mindful of TypeScript's Excess Property Checking](https://www.denhox.com/posts/be-mindful-of-typescripts-excess-property-checking/) - [TypeScript Handbook: Type Compatibility](https://www.typescriptlang.org/docs/handbook/type-compatibility.html)