The Case Against TypeScript (In Certain Contexts)
·PkgPulse Team
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.
Compare TypeScript adoption and health data for npm packages at PkgPulse.
See the live comparison
View typescript vs. javascript on PkgPulse →