Skip to main content

Command Palette

Search for a command to run...

TypeScript in 2026: What Changed, and the Patterns Worth Adopting

The Go-native compiler, what's still catching up, and nine patterns worth adding to your codebase today

Updated
•9 min read•View as Markdown
TypeScript in 2026: What Changed, and the Patterns Worth Adopting
Z
One platform to write, cross-post to Dev.to, Medium, WordPress, Hashnode, and Bluesky, and protect your SEO with canonical links. Zero paywalls and full content ownership.

TypeScript just went through the biggest change to its toolchain since it launched in 2012 — the compiler itself was rewritten from scratch. Here's what actually shipped, what's still catching up, and which patterns are worth adding to your code regardless of which version you're running.

The compiler got a decade's biggest upgrade

On July 8, 2026, TypeScript 7.0 reached general availability — the first stable release built on "Project Corsa," a full port of the compiler and language service from TypeScript-on-JavaScript to native Go. The headline number: Microsoft's own benchmark on the VS Code codebase showed full type-checking time dropping from 125.7 seconds to 10.6 seconds — roughly a 12x speedup, with Microsoft describing typical full-build gains as 8–12x.

The important part for how you think about adopting it: the language didn't change. The team preserved the same type-checking algorithms and semantics on purpose — they ported the implementation, not the design. Your .ts files and tsconfig.json options behave exactly the same; they just run faster.

The catch is tooling. TypeScript 7.0 doesn't yet ship a stable programmatic compiler API — that's targeted for version 7.1, which Microsoft has said is still months away. Anything that hooks directly into the compiler to do its job — Vue's Volar, Svelte's language tools, Astro's type checker, typescript-eslint, ts-jest — is pinned to TypeScript 6.0 until that API lands.

What this means practically:

  • Plain Node.js, React, and Next.js application code has no compiler-API dependency and can move to 7.0 today.

  • Teams on Vue, Svelte, or Astro, or anyone leaning on typescript-eslint, should stay on 6.0 for now.

  • You can run both in parallel: keep 6.x installed for emit and tooling while benchmarking 7.0's type-checking speed in CI.

What's worth keeping from 6.0

TypeScript 6.0 (March 2026) was the last release built on the old JavaScript-based compiler, and it wasn't just a bridge release:

  • Decorator metadata — Stage 3 decorators can now attach and read type metadata at runtime, powering a new generation of dependency-injection and validation libraries.

  • Import attributes — a standardized way to specify import conditions (like asserting a JSON import's type), replacing the older import-assertions syntax.

  • Sharper error messages — the compiler now suggests fixes for common mistakes like typos, instead of just flagging the problem.

Patterns worth knowing, whatever version you're on

Version churn aside, a handful of patterns consistently separate TypeScript that actually catches bugs from TypeScript that's just JavaScript with extra syntax.

1. satisfies over type annotations

Annotating a variable widens it to the declared type. satisfies checks a value against a type without throwing away the specific literal type TypeScript already inferred:

type Config = { mode: "development" | "production"; port: number };

const config = {
  mode: "development",
  port: 3000,
} satisfies Config;

// config.mode is still "development", not widened to `string`

2. Prefer unknown over any

any turns off type-checking for everything it touches, including code several calls downstream that has no idea it's operating on unchecked data. unknown forces you to prove what something is before you use it — exactly what you want for data crossing a boundary you don't control:

async function fetchUser(id: string): Promise<unknown> {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

function isUser(value: unknown): value is { id: string; name: string } {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    "name" in value
  );
}

const data = await fetchUser("abc");
if (isUser(data)) {
  console.log(data.name); // safe — narrowed by the type guard
}

3. Discriminated unions instead of optional-everything objects

Modeling state as one object with a pile of optional fields lets you construct impossible states — like loading: true and data both being set at once. A discriminated union makes those states unrepresentable, and pairing it with an exhaustiveness check means a new state added later can't silently fall through the cracks:

type RequestState =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: string[] }
  | { status: "error"; message: string };

function assertNever(value: never): never {
  throw new Error(`Unhandled case: ${JSON.stringify(value)}`);
}

function render(state: RequestState) {
  switch (state.status) {
    case "idle": return "Waiting to start";
    case "loading": return "Loading…";
    case "success": return state.data.join(", "); // data is known to exist here
    case "error": return state.message;
    default: return assertNever(state); // won't compile if a case is missing
  }
}

4. Branded types for values that are "just a string" until they aren't

IDs, emails, and currency amounts are usually strings or numbers at runtime, but treating them as interchangeable is how a postId ends up where a userId belongs. Branding catches it at compile time instead:

type UserId = string & { readonly __brand: "UserId" };

function getUser(id: UserId) { /* … */ }

const raw = "usr_123";
// getUser(raw); // Error: plain string isn't assignable to UserId

5. as const for precise literal inference

Without it, TypeScript widens array and object literals to their general type — string[], not the specific values inside it. as const locks in the literals, which is a clean way to derive a type from a single source of truth instead of maintaining the values and the type separately:

const ROUTES = ["/home", "/about", "/settings"] as const;
type Route = (typeof ROUTES)[number]; // "/home" | "/about" | "/settings"

function navigate(route: Route) { /* … */ }

navigate("/home");    // OK
// navigate("/missing"); // Error: not a valid Route

6. Utility types instead of duplicated shapes

Pick, Omit, Partial, and Record exist so your API-response type and your database type don't quietly drift apart:

interface User {
  id: string;
  name: string;
  email: string;
  passwordHash: string;
}

type PublicUser = Omit<User, "passwordHash">;
type ProfileUpdate = Partial<Pick<User, "name" | "email">>;

7. Clean up resources with using instead of manual try/finally

Explicit resource management gives anything disposable automatic cleanup — no more forgetting to close a connection on the unhappy path:

class DbConnection {
  [Symbol.dispose]() {
    console.log("connection closed");
  }
}

function runQuery() {
  using db = new DbConnection();
  // db's [Symbol.dispose]() runs automatically here, even if the line above throws
}

8. Validate data at runtime boundaries

Type annotations disappear the moment your code compiles. Promise<User> is a promise to the compiler, not a guarantee about what a server actually sent back. A schema-validation library lets you define a shape once, derive the TypeScript type from it, and get a real check at the one place your types could be lying to you — the network boundary:

import { z } from "zod";

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
});

type User = z.infer<typeof UserSchema>;

async function fetchUser(id: string): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  return UserSchema.parse(await res.json()); // throws if the shape doesn't match
}

9. Turn on the strict flags you're probably missing

strict: true is table stakes. Two flags people skip that catch real bugs:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true
  }
}

noUncheckedIndexedAccess makes array[i] return T | undefined instead of just T — which is what actually happens at runtime when the index is out of bounds. exactOptionalPropertyTypes stops { name?: string } from silently accepting an explicit { name: undefined }.

Common pitfalls to avoid

A few habits that quietly undermine everything above:

  • // @ts-ignore instead of // @ts-expect-error. @ts-ignore silences an error and stays silent forever, even if the surrounding code changes and the error turns into something worse. @ts-expect-error does the same suppression, but fails the build if there's no error left to suppress — so a stale suppression surfaces instead of hiding.

  • Reaching for as to make an error disappear. A type assertion tells the compiler "trust me," not "prove it." Occasionally necessary, but it's usually a sign a type guard or a schema check belongs there instead.

  • Turning off strict to unblock a migration and never turning it back on. A reasonable short-term move on a legacy codebase — just track it as debt with a ticket, not a permanent setting.

  • Letting inferred types balloon instead of naming them. If a function's return type is a three-line inline object, extracting it into a named type makes error messages and editor tooltips dramatically more readable.

A practical checklist for right now

  • Running Node, React, or Next.js with no compiler-API tooling? Try TypeScript 7.0 in a branch and benchmark your CI's type-check step.

  • Running Vue, Svelte, Astro, or relying on typescript-eslint? Stay on 6.0 and watch for the 7.1 API — Microsoft has said it's still some months out.

  • Either way: audit your tsconfig.json against the strict flags above. They're independent of which compiler you run.

  • Grep for any and as in your codebase. Each one is a candidate for unknown plus a type guard, or a schema validator at the boundary.

  • Adopt satisfies, discriminated unions, and branded types incrementally. None of them require a version bump — just a PR.

The takeaway

2026 is a good year to separate two questions that usually get bundled together: "should I upgrade the compiler" and "should I write better TypeScript." The compiler upgrade is largely a waiting game for your tooling to catch up. Writing better TypeScript — satisfies, unknown over any, exhaustive unions, branded types, using, boundary validation, and a stricter tsconfig — is available today, in whatever version you're already running.


Further reading: Announcing TypeScript 7.0 on the official TypeScript DevBlog.


Published via ZyVOP — Write once in Markdown, auto-backup to GitHub, and syndicate to Dev.to, Medium & Hashnode in 1 click.