The State of TypeScript in 2025: What’s Changed, What Hasn’t
TypeScript turned 13 this year. It is no longer the scrappy alternative to JavaScript type checkers — it is the default for any JavaScript project that expects to be maintained for more than six months. The State of JS 2024 survey showed 89% usage among professional developers, up from 78% in 2022. But the language and its ecosystem have shifted significantly in the past two years. Some changes are universally positive, some are contentious, and some old problems remain stubbornly unsolved. Here is what matters in 2025 if you are writing TypeScript professionally.
TypeScript 5.x: The Changes That Matter
TypeScript 5.0 through 5.5 shipped a series of incremental improvements that collectively changed how we write TypeScript at Harbor Software. The headline features that actually affected our codebase:
Decorators (ECMAScript standard). TypeScript 5.0 shipped support for the TC39 Stage 3 decorators proposal, replacing the experimental decorators that required experimentalDecorators: true in tsconfig. The new decorators are spec-compliant and work without any compiler flag. We migrated our NestJS backend and found that the new decorator semantics fixed three edge cases where our old experimental decorators had subtle initialization order bugs. The migration took a day — mostly updating the decorator factory signatures. One important note: NestJS and several other frameworks still use the legacy decorator syntax, so you may need to keep experimentalDecorators enabled for framework compatibility even though the standard decorators are technically better.
const type parameters. This feature (5.0) lets you infer literal types from function arguments without the caller using as const:
// Before 5.0 — caller must use "as const" to preserve literal types
function defineRoute<T extends readonly string[]>(paths: T) {
return paths;
}
const routes = defineRoute(['/', '/about', '/blog'] as const);
// Type: readonly ["/", "/about", "/blog"]
// After 5.0 — const modifier on type parameter does this automatically
function defineRoute<const T extends readonly string[]>(paths: T) {
return paths;
}
const routes = defineRoute(['/', '/about', '/blog']);
// Type: readonly ["/", "/about", "/blog"] — no "as const" needed
This is one of those features that sounds minor but eliminates friction in API design. Our configuration builder, route definitions, and schema validators all benefited from removing the as const requirement from caller code. Library authors benefit the most — your users no longer need to remember an unintuitive incantation to get the correct type inference.
Isolated declarations (5.5). This feature allows declaration file (.d.ts) generation to be parallelized across files because each file’s declarations can be computed independently without analyzing imports. The practical impact: our CI build’s type-checking step went from 47 seconds to 31 seconds. For monorepos with many packages, the improvement is larger because each package’s declarations can be generated in parallel. The constraint is that exported types must be explicitly annotated rather than inferred, which required us to add return type annotations to about 200 exported functions. This was tedious but also improved code readability — explicit return types serve as documentation.
Inferred type predicates (5.5). TypeScript can now infer type guard return types from function implementations:
// Before 5.5 — you had to write the type predicate explicitly
function isString(value: unknown): value is string {
return typeof value === 'string';
}
// After 5.5 — TypeScript infers the type predicate from the implementation
function isString(value: unknown) {
return typeof value === 'string';
}
// TypeScript infers: (value: unknown) => value is string
// This is especially useful for filter operations
const strings = mixedArray.filter(x => typeof x === 'string');
// Before 5.5: string[] only if you wrote a type predicate
// After 5.5: correctly inferred as string[]
This fixed one of the oldest TypeScript pain points. The Array.filter pattern with type narrowing “just works” now, without requiring a separate type guard function. We removed 34 explicit type predicate annotations from our codebase after upgrading to 5.5.
The Runtime Landscape: Node, Deno, and Bun
The TypeScript runtime story has fragmented in a positive way. Two years ago, Node.js was the only serious option. Today, all three runtimes are production-viable, and each has a distinct advantage.
Node.js 22+ added native TypeScript execution via type stripping. This means you can run node --experimental-strip-types app.ts without a build step. The limitation is that it only strips types — it does not handle TypeScript-specific syntax like enums, namespaces, or parameter properties. For our codebase, which avoids those features, this works perfectly for development. We still compile for production (tree-shaking, minification, etc.), but the development loop is now instant — save a file and re-run, no build step in between.
# Node.js 22 — run TypeScript directly (type stripping)
$ node --experimental-strip-types src/server.ts
# Works if you avoid: enums, namespaces, parameter properties, const enums
# Node.js 23+ — the flag is no longer experimental
$ node src/server.ts
# "Just works" for standard TypeScript that avoids TS-specific syntax
Deno 2 runs TypeScript natively (always has), added npm compatibility, and integrated with Node.js’s module resolution. We use Deno for our internal tooling scripts because its built-in formatter, linter, and test runner eliminate the need for separate ESLint, Prettier, and Jest configurations. For a CLI tool or utility script, deno run tool.ts with zero configuration is significantly faster to set up than a Node.js project with tsconfig, package.json, eslintrc, prettierrc, and jest config. Deno also has first-class support for running TypeScript with full type checking (not just stripping), which catches type errors at runtime that Node’s strip-types approach would miss.
Bun is the fastest runtime for TypeScript execution and package installation. We benchmarked our test suite: Jest on Node.js takes 34 seconds, Vitest on Node.js takes 22 seconds, and Bun’s built-in test runner takes 14 seconds. Bun’s package installation is also 3-5x faster than npm, which makes CI pipelines noticeably snappier. We use Bun in CI where speed matters and Node.js in production where ecosystem compatibility matters. The gap is narrowing as Bun improves Node.js API coverage, but we hit two compatibility issues in our last evaluation (a native addon for image processing that Bun does not support and a subtle difference in stream.Readable backpressure handling that caused data loss in our ingestion pipeline).
The Build Tool Shakeout
The TypeScript build tool landscape has consolidated dramatically. Two years ago, teams were choosing between tsc, esbuild, SWC, Rollup, Webpack, Vite, and tsup. In 2025, the choice is simpler and the answer depends on what you are building:
For applications (web apps, servers): Vite. It uses esbuild for development (fast HMR) and Rollup for production builds (optimized output). Vite 6 improved TypeScript support with better error reporting and faster rebuilds. Our web application builds went from 12 seconds with Webpack to 3.2 seconds with Vite. The migration from Webpack to Vite took 3 days, mostly spent on converting Webpack-specific plugin configurations.
For libraries: tsup. It wraps esbuild with sensible defaults for library publishing: dual CJS/ESM output, declaration file generation, and tree-shaking. Our npm packages use tsup and the configuration is typically 8 lines:
// tsup.config.ts — complete library build configuration
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['cjs', 'esm'],
dts: true,
clean: true,
splitting: false,
sourcemap: true
});
For monorepos: Turborepo. It orchestrates builds across packages with caching and parallelism. Combined with tsup for each package, our monorepo builds complete in 8 seconds with warm cache (down from 45 seconds with our previous Lerna + tsc setup). Turborepo’s remote caching feature means that CI builds are fast even on fresh machines because build artifacts are cached remotely and shared across CI runs.
For type checking: tsc is still the only option. None of the fast build tools (esbuild, SWC, Bun) perform type checking — they only strip types. You still need tsc --noEmit in your CI pipeline to catch type errors. This is a deliberate design choice: type checking is inherently slower than transpilation because it requires analyzing the entire program, not just individual files. Running tsc --noEmit in parallel with your fast build tool gives you both speed and correctness.
What Still Has Not Been Solved
Despite the progress, several TypeScript pain points remain unresolved in 2025:
Type-level performance. Complex type computations (deeply nested conditionals, recursive mapped types, template literal type unions) still cause the TypeScript compiler to slow down dramatically or hit the recursion limit. Our schema validation library uses recursive mapped types to infer types from runtime validators, and the compiler takes 8 seconds to check a file that imports 40+ schema definitions. The TypeScript team has improved performance in each release, but the fundamental issue — type checking is equivalent to computation, and complex types are equivalent to complex programs — has no silver bullet solution.
// This type is correct but makes the compiler sweat
type DeepPartial<T> = T extends object ? {
[P in keyof T]?: DeepPartial<T[P]>;
} : T;
// When applied to a deeply nested type with 50+ properties
// across 4 levels, type-checking this file takes 3+ seconds
type PartialConfig = DeepPartial<FullApplicationConfig>;
// Workaround: limit recursion depth manually
type DeepPartial<T, Depth extends number[] = []> =
Depth['length'] extends 4 ? T : // Stop at depth 4
T extends object ? {
[P in keyof T]?: DeepPartial<T[P], [...Depth, 0]>;
} : T;
ESM/CJS interop. The module system situation is better than it was in 2023 but still causes pain. Node.js requires .js extensions in ESM imports even when the source file is .ts. TypeScript’s moduleResolution: "bundler" mode papered over this for bundled applications, but libraries that need to support both CJS and ESM consumers still deal with configuration complexity. The dual-package hazard (where CJS and ESM versions of the same package are loaded simultaneously, creating duplicate state and broken instanceof checks) still catches teams who do not test both import styles. We test every library release with both require() and import in CI specifically because of this issue.
Runtime type checking. TypeScript’s types are erased at runtime. You cannot use a TypeScript interface to validate incoming API data without a separate validation library (Zod, Valibot, ArkType). Every TypeScript project ends up defining its types twice: once in TypeScript interfaces for compile-time checking and once in a validation schema for runtime checking. Projects like ArkType and Effect Schema are trying to unify these, but none have achieved the ecosystem adoption of Zod. This remains the most significant gap in the TypeScript developer experience. Zod 4 (currently in beta) promises better performance and TypeScript integration, but the fundamental duplication problem persists.
Error messages. TypeScript’s error messages for complex types are still notoriously unhelpful. When a type does not match, the error message dumps the full expanded type — sometimes hundreds of lines — instead of pointing to the specific property that differs. The community has built workarounds (pretty-ts-errors VS Code extension for readable error formatting, @total-typescript/ts-reset for fixing built-in type definitions), but the core problem is in the compiler’s error reporting logic. The TypeScript team is aware of this and has made incremental improvements, but a fundamental fix would require rethinking how the compiler presents type mismatches.
The Framework Ecosystem in 2025
TypeScript framework choices have matured significantly. The days of evaluating 8 different React state management libraries are largely over. The ecosystem has converged on clear winners for each use case, and the winners all have excellent TypeScript support.
Full-stack web: Next.js and Remix are the two serious contenders. Next.js 15 has first-class TypeScript support with typed route params, typed server actions, and typed API routes. Remix has similar TypeScript integration through typed loaders and actions. The choice between them is primarily about deployment model (serverless vs. long-running) and data fetching patterns (React Server Components vs. loader functions), not TypeScript support — both are excellent.
API servers: Fastify with @fastify/type-provider-typebox provides the best combination of performance and type safety for REST APIs. Hono has emerged as the best choice for edge-deployed APIs with its lightweight footprint and strong TypeScript types. NestJS remains popular for teams that prefer a batteries-included Angular-style framework with decorators and dependency injection. For GraphQL, Pothos (formerly GiraphQL) provides the best TypeScript-first schema builder, generating types from your resolver implementations rather than requiring you to maintain types separately from your schema.
ORMs and database: Drizzle ORM has overtaken Prisma as the preferred TypeScript ORM for new projects in our team. Drizzle generates types from your schema definition (SQL-like TypeScript DSL), supports edge runtimes (unlike Prisma, which requires a binary engine), and produces SQL that closely matches what you would write by hand. Prisma is still excellent for rapid prototyping and has better documentation, but Drizzle’s performance and runtime compatibility advantages make it our default for production applications. For teams that want maximum control, Kysely provides a type-safe query builder without the ORM abstraction, generating TypeScript types from your database schema.
Testing: Vitest has effectively replaced Jest for TypeScript projects. It is faster (native ESM, no transform step for TypeScript), has better TypeScript integration (type-checked assertions with expectTypeOf), and is compatible with most Jest APIs so migration is straightforward. We migrated our main project from Jest to Vitest in 4 hours with only 3 test files needing changes. Playwright remains the standard for E2E testing, and its TypeScript types are among the best in any testing library.
The common thread across these choices: TypeScript support is no longer a differentiator. Every serious JavaScript framework has excellent TypeScript types. The differentiators are now performance, developer experience, and deployment model compatibility. TypeScript’s ubiquity has raised the bar — frameworks without good TypeScript support simply do not get adopted.
Our TypeScript Standards in 2025
Based on six years of TypeScript in production at Harbor Software, here are the standards we enforce across all projects:
// tsconfig.json — our base configuration for 2025
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"verbatimModuleSyntax": true,
"isolatedDeclarations": true,
"skipLibCheck": true
}
}
The non-obvious choices: noUncheckedIndexedAccess makes array and record access return T | undefined instead of T, catching a class of bugs where code assumes an array index is in bounds or a record key exists. This flag catches 2-3 bugs per month in our codebase — array-out-of-bounds access and missing record keys that would otherwise be runtime errors. exactOptionalPropertyTypes distinguishes between “property is missing” and “property is present but undefined” — a real semantic difference in APIs where undefined values in JSON payloads behave differently from absent keys. verbatimModuleSyntax requires explicit import type for type-only imports, which improves build tool compatibility and makes the import intent clear to both humans and tools.
We also enforce these conventions across all projects:
- No enums. Use
as constobjects andtypeofextraction instead. Enums generate runtime code, have surprising behavior with numeric values, and are not compatible with Node.js type stripping.as constobjects are purely type-level and work everywhere. - Explicit return types on exported functions. Required by
isolatedDeclarationsand good practice for API boundaries. Internal functions can rely on inference. - No
anyexcept in type assertions for genuinely untyped third-party code. We useunknownfor truly unknown values and narrow with type guards. Our ESLint config flagsanyas an error except in files matching*.d.tsor*.test.ts. - Branded types for domain identifiers.
UserId,OrderId, andModelIdare branded string types that prevent accidentally passing a user ID where an order ID is expected. This catches a class of bugs that nominal type systems prevent automatically but TypeScript’s structural type system allows.
// Branded types — prevent ID mixups at compile time
type Brand<T, B> = T & { __brand: B };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
function getUser(id: UserId): Promise<User> { ... }
function getOrder(id: OrderId): Promise<Order> { ... }
const userId = '123' as UserId;
const orderId = '456' as OrderId;
getUser(userId); // OK
getUser(orderId); // Compile error! OrderId is not assignable to UserId
TypeScript in 2025 is mature, fast enough for most workloads, and supported by every major runtime. The rough edges that remain — type performance, ESM/CJS interop, runtime validation — are known quantities with established workarounds. If you are starting a new JavaScript project today and not using TypeScript, you are making a choice that your future maintainers will question.