JavaScript vs TypeScript: Which One Should You Actually Use?
TypeScript is JavaScript with types. That's the short answer. The real answer involves a friend who was right about something I didn't want to hear, a production bug that types would have caught immediately, and the slow realization that the compiler was never the enemy.
Every technology company, at some point, has this argument. The JavaScript team pushes back: types slow down iteration, the build step adds complexity, and experienced developers know their code. The TypeScript advocates counter: runtime type errors are worse than compile-time errors, the IDE experience is better, and the refactoring safety pays for itself.
Both sides are arguing from real experience. The useful answer requires knowing what the tradeoff actually is.
The Fundamental Difference
JavaScript is dynamically typed. The runtime figures out types when it needs to. You pass a string where a function expects a number, and JavaScript will try to make it work through coercion. Sometimes it succeeds in surprising ways. Sometimes it fails at runtime in a way that affects production users.
TypeScript is JavaScript with a static type system. You annotate your code with types, and a compiler checks those annotations before the code ever runs. Type errors surface in your editor while you write the code, not in a production log at 2 AM.
TypeScript compiles to JavaScript. The output is standard JavaScript that runs in any browser or Node.js. TypeScript types don't exist at runtime. The type system is a development-time tool, not a runtime enforcement mechanism.
This means choosing TypeScript doesn't mean choosing a different runtime or ecosystem. It means adding a compile step and type annotations to the JavaScript you were already writing.
What TypeScript Actually Catches
The value of TypeScript isn't abstract. It catches specific categories of bugs.
Typos in Property Names
// JavaScript — no error until runtime
const user = { name: "Traven", role: "admin" };
console.log(user.nme); // undefined — typo, silently fails
// TypeScript — error immediately
const user = { name: "Traven", role: "admin" };
console.log(user.nme); // Error: Property 'nme' does not exist on type '...'
Passing the Wrong Type
// JavaScript — no complaint
function greet(name) {
return `Hello, ${name.toUpperCase()}`;
}
greet(42); // TypeError at runtime: name.toUpperCase is not a function
// TypeScript — caught before execution
function greet(name: string): string {
return `Hello, ${name.toUpperCase()}`;
}
greet(42); // Error: Argument of type 'number' is not assignable to parameter of type 'string'
Null and Undefined Access
With strictNullChecks enabled, TypeScript tracks whether a value might be null or undefined and forces you to handle it.
function getUser(id: number): User | null {
return db.find(id); // might return null
}
const user = getUser(1);
console.log(user.name); // Error: Object is possibly 'null'
// TypeScript forces you to handle the null case
if (user) {
console.log(user.name); // Safe
}
Refactoring Safety
This is TypeScript's highest-value feature for larger codebases. When you rename a function, change a type's shape, or move code, TypeScript traces every reference and flags anything that breaks. JavaScript gives you grep and prayer.
interface UserConfig {
displayName: string; // Rename this field
email: string;
}
// TypeScript immediately shows every place that references `displayName`
// and flags them as errors after the rename
The Real Costs of TypeScript
TypeScript advocates sometimes undersell the actual costs.
The Build Step
TypeScript doesn't run directly in browsers or in Node.js (without extra tooling). You need a compilation step. In a browser project, your bundler (Vite, Webpack, esbuild) handles this invisibly. In a Node.js project, you need tsc, tsx, or a similar tool.
For a small script, this is friction. For a large application, the build step is already there regardless. The cost scales inversely with project size.
Deno and Bun eliminate this. Both run TypeScript natively. If you're on either runtime, the build overhead disappears. See What is Deno and What is Bun.
Type Annotation Overhead
You write more code. Every function parameter ideally has a type. Every returned value ideally has a type. Complex data structures need interfaces or type aliases.
For exploratory code and prototypes, this overhead is real. You want to figure out the shape of the data before committing to types. TypeScript has any for this (which disables type checking for a value) and gradual migration support, but there's still an upfront cost.
The Learning Curve
TypeScript's type system goes deep. Basic types are easy. Generics require some learning. Conditional types, mapped types, template literal types, infer keywords — these take time to understand. You can ship useful TypeScript without knowing the advanced features, but a poorly typed codebase offers reduced safety.
Configuration Complexity
tsconfig.json has dozens of options. Getting a TypeScript project set up correctly — especially with ESM, path aliases, and declaration files — involves more configuration than a plain JavaScript project.
When JavaScript Makes Sense
Scripts and automation. A 50-line script that runs once a day doesn't need a type system. The overhead of TypeScript setup exceeds the safety benefit for small, isolated tools.
Rapid prototyping. When you're figuring out the shape of a problem, types slow you down. Write it in JavaScript first. Convert to TypeScript when the design stabilizes.
Learning. If you're new to programming, adding TypeScript's type system on top of JavaScript's runtime model is two things to learn at once. JavaScript first, TypeScript after.
Team context. A team with no TypeScript experience will write poor TypeScript faster than they'll write good JavaScript. Type annotations that aren't accurate are worse than no annotations.
Small, self-contained modules. A 200-line utility module used in one place, by one person, doesn't need the refactoring safety that TypeScript provides at scale.
When TypeScript Makes Sense
APIs and shared libraries. Any code that other code calls benefits from type annotations. The types become documentation. The compiler enforces the contract.
Teams. Multiple people working on the same codebase, especially when people join and leave, need types to communicate intent. TypeScript makes unfamiliar code readable at a glance.
Long-lived projects. The longer a codebase runs, the more valuable refactoring safety becomes. JavaScript codebases rot. TypeScript codebases rot slower because the compiler keeps dependencies honest.
Data-heavy applications. Database responses, API payloads, form data — anywhere you're transforming data across boundaries, TypeScript catches mismatches at compile time rather than in production.
Anything that talks to external services. Typed API clients, database query builders, and schema validation libraries (like Zod) interact with TypeScript's type system to give you safety across system boundaries.
Gradual Migration
You don't choose between JavaScript and TypeScript once and live with it forever. You can adopt TypeScript incrementally.
The allowJs option in tsconfig.json lets TypeScript and JavaScript files coexist in the same project. You rename files from .js to .ts one at a time. The compiler checks the TypeScript files strictly and ignores the JavaScript files (unless you also enable checkJs).
{
"compilerOptions": {
"allowJs": true,
"checkJs": false,
"strict": true
}
}
For JavaScript files you want to check without converting, @ts-check at the top of the file enables basic checking through JSDoc comments.
This path — start with JavaScript, add TypeScript as the codebase grows — is how many large projects transitioned. Airbnb migrated their JavaScript frontend to TypeScript over several years, not in one pull request.
The Practical Answer
For greenfield projects with more than two developers and a planned lifespan of more than a year, use TypeScript. The overhead at the start is real. The safety during growth is worth it.
For scripts, prototypes, personal tools, and projects where you're the only developer, JavaScript is fine. Don't add complexity you don't need.
For Node.js projects, TypeScript with tsx for development makes the build step mostly invisible. See What is Node.js for the full setup.
For Deno or Bun projects, use TypeScript. Both runtimes execute it natively, the overhead is zero, and the safety is free.
I spent years arguing against TypeScript. I was wrong, and I documented that conversion in What is TypeScript. The type system is not the enemy. The bugs it prevents are worse than the annotations it requires.
Performance
TypeScript has no runtime performance cost. The types are stripped at compilation. The JavaScript that runs is identical to what you'd have written without types. The only overhead is the compile step itself, which doesn't affect production performance.
Tooling Support
Both languages have excellent editor support in VS Code, WebStorm, and Neovim with LSP. TypeScript support is better for finding references, automated refactoring, and understanding unfamiliar code. The gap isn't as large as it was in 2018, but TypeScript's IDE experience is still noticeably better.
The Other Articles in This Series
- What is JavaScript — the fundamentals and mental model
- What is TypeScript — the type system in depth
- What is Node.js — JavaScript on the server
- Node.js vs Deno vs Bun — choosing a runtime