Skip to main content

The Case Against TypeScript (In Certain Contexts) 2026

·PkgPulse Team
0

TL;DR

TypeScript should be your default for any production JavaScript project, BUT there are specific contexts where the cost exceeds the benefit. The cases: small scripts/utilities (type overhead > value), early-stage prototyping (types slow iteration), pure Node.js scripts without editor integration, and teams without TypeScript experience (learning curve costs more than bugs prevented). This isn't a case against TypeScript generally — 95% of projects benefit from it. This is about the 5% where you're adding friction to solve a problem that doesn't exist.

Key Takeaways

  • TypeScript wins for: production apps, shared libraries, team projects, anything lasting >6 months
  • TypeScript may not help for: one-off scripts, early prototypes, simple APIs, small solo projects
  • The real cost: build step complexity, learning curve, type overhead, "fighting the type checker"
  • Modern alternatives: JSDoc types (TypeScript checking without the syntax), d.ts files
  • The decision: Will this code last? Will others read it? Is there a complex domain? → TypeScript

The Case FOR TypeScript (Quick)

Before the hot take, let's be honest about why TypeScript won:

1. Catch entire categories of bugs at compile time
   → null/undefined access errors: eliminated
   → Wrong property names: eliminated
   → Function signature mismatches: eliminated
   → These are common bugs that don't require tests to catch

2. Tooling quality is dramatically better
   → Autocomplete knows the shape of every object
   → Refactoring is safe (rename property → all usages updated)
   → Navigate to definition works across packages
   → Inline documentation from types

3. Self-documenting code
   → Function signature IS the documentation
   → No need to read the implementation to understand the API
   → New team members onboard faster

4. npm ecosystem has shifted
   → 95% of top packages ship TypeScript types
   → @types/* packages for the rest
   → Using an untyped package is now unusual

TypeScript adoption: 68% of JavaScript developers (State of JS 2025).
For new production projects: ~80%+ use TypeScript.
This is the right default. The rest of this article is the exceptions.

Context 1: One-Off Scripts and Automation

# You need to: rename 500 files, transform a CSV, scrape a page
# This script will run once and be deleted
# You're the only person who will read it

# TypeScript overhead:
# → tsconfig.json setup
# → Build step (tsc or ts-node)
# → Type annotations on every variable
# → Fighting "any" vs properly typed edge cases
# → .js vs .ts output directory confusion

# JavaScript alternative:
#!/usr/bin/env node
// scripts/migrate-data.js
const fs = require('fs');
const csv = require('csv-parse/sync');

const data = fs.readFileSync('data.csv', 'utf8');
const rows = csv.parse(data, { columns: true });

rows.forEach(row => {
  // process row
});

// 10 lines, no build step, no type overhead.
// This is the right choice for a one-off script.

# When to use TypeScript anyway:
# → The script will become a recurring tool
# → Multiple people will run/modify it
# → It interfaces with complex external APIs
# → It lives in a monorepo that already has TypeScript

# The honest rule:
# Lifespan < 1 week, single author, no complex logic = JavaScript
# Anything else = TypeScript

Context 2: Early-Stage Prototyping

// You're exploring: does this API work? Can this algorithm solve the problem?
// You're changing the shape of your data 10 times per hour.
// You don't know what the types ARE yet.

// TypeScript friction during exploration:
interface UserData {
  id: string;
  name: string;
  // ... but wait, I need to add email, but do I have it everywhere?
  // And now I'm making everything optional: email?: string
  // But that breaks 15 usages
  // Now I need to fix all the type errors before I can test my idea
}

// The alternative: JavaScript with // @ts-check
// @ts-check
// @ts-ignore (selectively suppress errors you'll fix later)

// Or: use any liberally in prototype, add types when stabilizing
function processUser(user: any) {  // "any" is fine for prototypes
  return user.doSomething();  // Explore freely, type later
}

// The workflow that works:
// Phase 1 (exploration): TypeScript with liberal `any`, or plain JavaScript
// Phase 2 (stabilization): Add proper types as the shape stabilizes
// Phase 3 (production): Strict TypeScript (strict: true)

// The mistake: applying strict TypeScript constraints before you know the shape.
// You'll spend more time fixing type errors than testing your idea.
// Ideas should die fast. Type-correct ideas die slow.

Context 3: The Over-Engineering Trap

// TypeScript can become the problem when you start solving
// type system puzzles instead of application problems.

// Over-engineered (solving a type problem, not an app problem):
type DeepPartial<T> = T extends object
  ? { [P in keyof T]?: DeepPartial<T[P]> }
  : T;

type UnionToIntersection<U> =
  (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;

type IsNever<T> = [T] extends [never] ? true : false;

// This is TypeScript as a puzzle. Some engineers love this.
// The question: does your application benefit from this?
// Usually: no. The complexity is in the types, not the app.

// Simpler and often better:
// 1. Generic functions with <T>
// 2. Union types (A | B)
// 3. Optional properties (?:)
// 4. Discriminated unions ({ type: 'a', ... } | { type: 'b', ... })
// 5. Type assertions (as) when you know more than the compiler

// The 80% rule:
// 80% of your TypeScript value comes from:
// → Typed function parameters and return types
// → Typed object shapes (interfaces)
// → Null safety (strictNullChecks)
// → Generic containers (Array<T>, Promise<T>, Map<K,V>)

// The last 20% (conditional types, mapped types, infer, etc.) adds
// significant complexity for marginal additional safety.

// Know where on the complexity spectrum you need to be.

Context 4: The Learning Curve Is Real

TypeScript has a real learning curve that matters for teams:

Beginner JavaScript developer:
→ Learns JS basics: functions, arrays, objects, async/await
→ Then must learn: type annotations, interfaces, generics, enums, decorators
→ Mental models shift: "why can't I pass this object to this function?"
→ New bugs: type errors that are confusing until you understand the type system
→ Time to productive: +2-4 weeks vs plain JavaScript

Junior developer on TypeScript codebase (no TypeScript experience):
→ Types feel like obstacles: "why do I need to type this?"
→ Tends to reach for `any` to silence errors
→ "any everywhere" TypeScript is worse than plain JavaScript
→ Type errors are mystifying without understanding variance, generics
→ Productivity drops until TS mental model is internalized

When this matters:
→ High-turnover teams (constant onboarding)
→ Hackathon/startup teams moving very fast
→ Outsourced development teams with varying TypeScript skills
→ Open source projects where contributors have varying experience

The solution isn't "avoid TypeScript" — it's:
→ Invest in TypeScript training before requiring it
→ Start with less strict settings (strict: false → strict: true over time)
→ Code review TypeScript PRs with types, not just logic
→ Document your team's TypeScript patterns

But: acknowledge the real cost before requiring TypeScript everywhere.

JSDoc: TypeScript's Secret Weapon for Mixed Environments

// JSDoc types: TypeScript type checking without TypeScript syntax
// Your files are .js but the IDE gives you TypeScript-quality tooling

// math.js
/**
 * @param {number[]} numbers
 * @returns {number}
 */
export function sum(numbers) {
  return numbers.reduce((acc, n) => acc + n, 0);
}

/**
 * @typedef {Object} User
 * @property {string} id
 * @property {string} name
 * @property {string} [email]
 */

/**
 * @param {User} user
 * @returns {string}
 */
export function formatUser(user) {
  return `${user.name} (${user.id})`;
}

// Enable checking in .js files via jsconfig.json:
{
  "compilerOptions": {
    "checkJs": true,
    "allowJs": true,
    "strict": true,
    "outDir": "./dist"
  },
  "include": ["src/**/*.js"]
}

// Result: TypeScript error checking WITHOUT:
// → .tsx/.ts file extension
// → Build step (can run directly with node)
// → Type annotation syntax in the code
// → tsconfig.json complexity

// Use cases:
// → Migrating a JavaScript codebase gradually
// → Scripts that need editor support but not build step
// → Libraries that want to support both JS and TS consumers
// → Configuration files (vite.config.js, next.config.js)

// This is actually how Vite, Svelte, and Vue 3 core are typed internally.

The Actual Decision Framework

Use TypeScript when (you should default to this):
✅ Production web application
✅ Library or package published to npm
✅ Team of 2+ developers
✅ Code expected to live >3 months
✅ Complex domain model (e-commerce, finance, healthcare)
✅ API integration with complex response shapes
✅ You have TypeScript experience on the team

Consider JavaScript (or JSDoc) when:
⚠️  One-off scripts that will run once and be deleted
⚠️  Very early prototype (will be rewritten anyway)
⚠️  Solo project with simple logic and you know the codebase
⚠️  Team has NO TypeScript experience and no time to learn
⚠️  Pure Node.js scripts with no complex logic

Never use JavaScript (TypeScript is clearly better):
❌ Shared libraries or utilities
❌ Anything with complex data transformations
❌ Anything that other developers will maintain
❌ Anything that integrates with external APIs
❌ Anything with business logic

The honest summary:
TypeScript's reputation as "always the right choice" is mostly earned.
The cases where it's not are narrow and specific.
If you're unsure, use TypeScript.
The cost of switching from JS to TS later is higher than starting with TS.

The Hidden Costs: Build Complexity, Type Errors, Compilation

TypeScript advocates are generally honest about the benefits and quick to minimize the costs. The costs are real and worth stating plainly.

Build pipeline complexity is the most immediate. TypeScript requires a compilation step — tsc, esbuild, SWC, or Babel with @babel/preset-typescript — that plain JavaScript doesn't. In a simple single-package app this is a one-line config change. In a monorepo, each package needs its own tsconfig.json, potentially its own tsconfig.build.json separate from the one used for type checking, and its own compilation pipeline. Project references help but add their own configuration surface area. Teams routinely spend non-trivial time on "why is this import not resolving" problems that are purely TypeScript configuration issues, not application logic.

The error cost is less discussed. A TypeScript compiler error that blocks your build can be more disruptive to development flow than the runtime error it was preventing. Complex generic type errors — especially in upgraded library versions where the type signatures changed — can take hours to resolve. The equivalent JavaScript issue either never surfaces (the types were overly conservative) or surfaces as a runtime error that stack-traces to the exact line in 30 seconds. Neither outcome is always better, but the TypeScript path has a longer tail when types go wrong.

The inference wall compounds this. TypeScript's inference is genuinely powerful, but it has limits. When inference breaks down on complex conditional types, deeply nested generics, or heavily overloaded function signatures, you have to write explicit type annotations that can be harder to read than the untyped code. Mapped types and conditional types, in particular, produce error messages that are nearly unreadable without deep TypeScript knowledge.

The @ts-ignore tax is the worst outcome: codebases that add @ts-ignore or any liberally to silence errors get a false sense of type safety while paying every compilation cost and getting none of the correctness guarantees.

The honest assessment: TypeScript pays off for medium-to-large codebases with teams of three or more developers who plan to maintain the code for two or more years. For solo-dev scripts, hackathon projects, and short-lived tools, the cost-benefit frequently doesn't justify it.


The 2026 Decision Framework: TypeScript by Default

The default in 2026 should be TypeScript. That is the right starting point. The question isn't "TypeScript or not" — it's "which specific contexts justify an exception."

New projects with more than one developer, or with an expected lifespan beyond six months, should start with TypeScript. The cost of migrating a JavaScript codebase to TypeScript later is significantly higher than starting typed: you're adding annotations to code whose behavior you may no longer fully understand, and the compiler will surface ambiguities that accumulated silently over months.

The exceptions that legitimately justify plain JavaScript: shell scripts and deployment automation (tsx is overkill for a 50-line script that runs once in CI), throwaway prototypes where you're testing a business hypothesis rather than a technical one (types slow iteration when the data shape is changing ten times an hour), projects where the team's TypeScript skills are insufficient to use it properly (bad TypeScript — full of any and @ts-ignore — is strictly worse than well-written JavaScript), and tightly scoped published utilities where TypeScript's configuration complexity adds more friction for contributors than the types provide value.

The JSDoc middle path is underused and worth knowing. For contexts where TypeScript's compilation overhead is genuinely prohibitive, JSDoc type annotations provide roughly 80% of TypeScript's IDE experience with zero build step. VS Code provides full IntelliSense, autocomplete, and type-error highlighting from JSDoc comments. TypeScript's checkJs: true compiler option runs full type checking on plain JavaScript files with JSDoc annotations — no .ts extension required, no compilation step needed to run the file. This is how Vite's internal configuration and many utility scripts are typed.

The recommendation: adopt TypeScript by default, use JSDoc for scripts and one-off tools, and be honest with yourself about whether "I don't want to deal with TypeScript" is a legitimate technical decision for your specific context or a preference that will cost the team later.


The Runtime Cost of TypeScript Tooling in CI

TypeScript's benefit is compile-time safety, but that compile-time has a cost that scales with codebase size and CI pipeline frequency. For large codebases running CI on every pull request, type-checking time is a non-trivial component of the feedback loop. Understanding where that time goes — and what tradeoffs exist — is part of making an informed decision about TypeScript configuration.

The TypeScript compiler does two distinct things that are often conflated: type checking and transpilation. In most modern setups, transpilation — converting TypeScript syntax to JavaScript — is delegated to a fast tool like esbuild, SWC, or Babel with the TypeScript plugin. These transpilers strip types without checking them, running in milliseconds for large codebases. The actual type checking — running tsc --noEmit — is the slow step, and it scales roughly with the size of the type graph rather than just the line count. A codebase with deeply interlinked generics, many conditional types, and extensive use of mapped types can take minutes to type-check even at moderate file counts.

Teams in large TypeScript codebases frequently encounter a configuration decision: run tsc --noEmit in a separate CI step, or skip full type checking and rely on IDE error reporting. Skipping full CI type checking creates a divergence between what the IDE shows and what would fail a strict compiler pass — a silent degradation that becomes visible when a new developer joins without the IDE configured properly, or when a PR adds a type error that nobody's editor caught because it was in a file nobody opened during that PR.

The mitigation strategies are well-known: TypeScript project references allow incremental type checking across package boundaries, caching previous type-check results. Tools like tsc-alias and ts-project-references improve monorepo performance. But these add configuration complexity. The net result is that for very large codebases, TypeScript type checking is a continuous investment in CI infrastructure, not a one-time setup cost.


When Dynamic Typing Is a Feature, Not a Bug

Static typing solves certain categories of problems definitively: it prevents wrong-shape-object errors, wrong-type-argument errors, and missing-property errors from reaching runtime. For application code that processes user input, integrates with external APIs, and enforces business rules across a team, these are the right problems to solve.

But there are contexts where the flexibility of dynamic typing is genuinely useful — not as a workaround for a limitation, but as a first-class feature of the solution. Data transformation pipelines are the clearest example. A script that reads JSON from one format, reshapes it, and writes it in another format is doing inherently dynamic work. The schema of the input may be partially unknown or variable across files. Adding TypeScript types to every intermediate transformation shape adds annotation overhead proportional to the number of transformation steps, with correctness benefit that often falls short of what runtime validation provides.

Scripting and automation contexts present similar dynamics. When a developer writes a deployment script that reads environment variables, calls APIs, and chains outputs together, the type annotations on environment variable shapes and API responses add maintenance overhead every time the script is updated or the API changes. The script runs once per deployment, often without tests, by a single author who knows the data shapes. The TypeScript investment returns less value here than in a shared library with complex business logic.

Rapid prototyping where the data model is actively evolving is a third case. When you're iterating on a data structure ten times in a day — adding fields, changing nesting, renaming properties — TypeScript type errors become a continuous interruption. The types aren't documenting a stable API; they're adding friction to an exploration process. Using any or JavaScript in this phase, then adding types when the model stabilizes, is a legitimate workflow, not a shortcut.

The distinction worth drawing is between contexts where dynamic typing enables agility and contexts where it enables sloppiness. Scripting, data transformation, and early prototyping are cases where agility is the goal. Production application logic handling user-facing features across a team is where the discipline of static typing pays off. The mistake is applying a single policy across both contexts.


TypeScript's False Security: Types Don't Guarantee Runtime Correctness

The most consequential misunderstanding about TypeScript is the implicit assumption that a codebase that type-checks has substantially fewer bugs than an equivalent JavaScript codebase. TypeScript prevents a specific and important class of bugs — type mismatches at call sites, missing properties, wrong argument counts. It does not prevent logic errors, race conditions, incorrect business rule implementations, off-by-one errors, or failures in code paths that are technically type-correct but semantically wrong.

A TypeScript function that passes user.balance - deductionAmount where balance and deductionAmount are both correctly typed as number will compile without errors whether the subtraction is correct or incorrectly ordered. A function that returns null for a case that should throw will compile without errors if the return type permits null. An async function that drops a promise without await is a common bug that TypeScript does not prevent in most configurations.

The deeper issue is that TypeScript types at the API boundary don't guarantee that runtime data matches those types. External API responses, database query results, and environment variables are all typed via assertion or inference from Zod/similar — but if the actual data diverges from the declared type at runtime, TypeScript has provided false confidence. The safe pattern is to validate with a runtime schema validator at every boundary where untrusted data enters the system. When teams do this correctly, TypeScript provides genuine end-to-end correctness. When they don't — when they trust an as ApiResponse cast without validation — TypeScript's type-checked guarantee stops at the cast.

The practical implication is that TypeScript is a valuable tool that needs to be combined with runtime validation and meaningful test coverage to deliver on its correctness promise. Teams that adopt TypeScript and reduce their test investment because "TypeScript catches the bugs" are trading one form of assurance for another that covers a different, narrower set of failure modes.


Migration Cost Calculations

The cost of migrating a JavaScript codebase to TypeScript is often underestimated in planning discussions. The common framing is "we'll add types incrementally" — which is true, but the incrementalism conceals the full scope of work.

An incremental migration using allowJs: true and checkJs: false means TypeScript files and JavaScript files coexist, with TypeScript providing types for typed files and no checking for the rest. This works, but it creates a bifurcated codebase where the benefits of TypeScript are partial until the migration is complete. Any JavaScript file that imports from a TypeScript file gets types; any TypeScript file that imports from a JavaScript file gets untyped inputs unless the JavaScript file has JSDoc annotations or a .d.ts declaration file is written for it.

The typical migration rate for a codebase being incrementally migrated is 2–5 files per engineer per week when the migration is a background task alongside feature work. A codebase with 500 JavaScript files will take 100–250 engineer-weeks of migration effort in background mode. In practice, migrations often stall at 60–80% completion when the remaining files are either complex to type correctly, or owned by teams who haven't prioritized the migration. Partially migrated codebases carry the overhead of TypeScript tooling without the full benefit of end-to-end type safety.

The alternative — a focused migration sprint — allows a small team to migrate a 200-file codebase in 2–4 weeks of concentrated effort, emerging with a fully typed codebase and consistent tooling. The tradeoff is the upfront time investment rather than sustained background overhead. For new projects deciding between starting in TypeScript or starting in JavaScript with a plan to migrate later, the math favors starting typed: the cost of adding types to code you wrote last week is lower than adding types to code you wrote 18 months ago.


Team Contexts Where TypeScript Adds Net Negative Value

The honest case against TypeScript is narrower than its critics suggest, but it is real in specific team contexts. Dismissing these contexts as edge cases understates the genuine cost TypeScript imposes when the organizational conditions are wrong.

High-turnover teams with no TypeScript training pipeline present the clearest case. When the average engineer on a team has six months of TypeScript experience or less, the team is continuously in the steep part of the TypeScript learning curve. Developers suppress errors with any rather than solving them correctly. They copy TypeScript patterns without understanding the underlying type model. The result is a codebase that uses TypeScript syntax without delivering TypeScript's correctness guarantees — and that imposes all the build-step overhead without the safety payoff.

Outsourced and contractor-heavy development contexts face a related problem. Contractors with variable TypeScript proficiency introduce inconsistent type usage — overly permissive in some places, over-engineered in others. Code review that focuses on TypeScript correctness adds overhead to every PR, and reviewers without deep TypeScript expertise may miss type-unsafe patterns that look plausible.

Teams building internal tooling for non-technical stakeholders, where the primary requirement is iteration speed and the codebase is maintained by one or two developers indefinitely, often see better outcomes with well-tested JavaScript. The type system's primary value — communication of intent across a team and across time — is compressed when the team is one person and the time horizon is "as long as it takes to complete the project."

The key distinction is whether the team has the TypeScript expertise and training infrastructure to use it correctly. TypeScript used incorrectly — with any escape hatches, suppressed errors, and shallow type annotations — imposes all of its costs while delivering a fraction of its value. For teams without the expertise to use it well, the honest choice is either to invest in that expertise before requiring TypeScript, or to acknowledge that JavaScript with good tests is a better fit for the team's current capabilities.


Compare TypeScript adoption and health data for npm packages at PkgPulse.

Compare Typescript and Javascript package health on PkgPulse.

See also: AVA vs Jest and Bun Test vs Vitest vs Jest 2026: Speed Compared, AI SDK vs LangChain: Which to Use in 2026.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.