TypeScript vs JavaScript: The Core Difference
TypeScript is a statically typed superset of JavaScript that compiles to plain JavaScript. Every valid JavaScript file is also valid TypeScript, but TypeScript adds optional type annotations, interfaces, enums, generics, and compile-time error checking. The key question is not whether TypeScript is "better" -- it is about when the overhead of types pays off and when it does not.
This guide provides a practical comparison with real code examples, performance analysis, ecosystem considerations, and a clear decision framework to help you choose the right language for your next project.
Quick Comparison Overview
| Aspect | JavaScript | TypeScript |
|---|---|---|
| Typing | Dynamic (runtime) | Static (compile-time) |
| Error detection | At runtime | Before running code |
| Learning curve | Lower | Higher (types, generics) |
| Build step | None (or optional bundler) | Required (tsc, esbuild, etc.) |
| Runtime | Browser, Node.js, Deno, Bun | Same (compiles to JS) |
| Type definitions | JSDoc (optional) | Native syntax |
| Refactoring | Error-prone at scale | Safe with compiler checks |
| IDE support | Good | Excellent (autocomplete, errors) |
| Ecosystem | Full npm access | Full npm + @types packages |
| File extension | .js, .mjs, .cjs | .ts, .tsx, .mts |
Side-by-Side Code Comparison
Variables and Functions
// JavaScript
function calculateTotal(items, taxRate) {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0) * (1 + taxRate);
}
const user = {
name: "Alice",
age: 30,
email: "alice@example.com"
};
// No errors until runtime if you pass wrong types
calculateTotal("not an array", "not a number"); // Runtime error!// TypeScript
interface CartItem {
name: string;
price: number;
quantity: number;
}
interface User {
name: string;
age: number;
email: string;
}
function calculateTotal(items: CartItem[], taxRate: number): number {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0) * (1 + taxRate);
}
const user: User = {
name: "Alice",
age: 30,
email: "alice@example.com"
};
// Compile-time error! Caught before running
calculateTotal("not an array", "not a number");
// Error: Argument of type 'string' is not assignable to parameter of type 'CartItem[]'API Response Handling
// JavaScript - No guarantee on response shape
async function fetchUser(id) {
const res = await fetch(`/api/users/${id}`);
const data = await res.json();
// What properties does 'data' have? No way to know without docs.
// data.name? data.username? data.fullName?
console.log(data.name); // Could be undefined
console.log(data.posts.length); // Could crash if posts is null
}// TypeScript - Response shape is documented and enforced
interface ApiUser {
id: number;
name: string;
email: string;
posts: Post[];
createdAt: string;
}
interface Post {
id: number;
title: string;
published: boolean;
}
async function fetchUser(id: number): Promise<ApiUser> {
const res = await fetch(`/api/users/${id}`);
const data: ApiUser = await res.json();
// IDE autocomplete shows all available properties
console.log(data.name); // string - guaranteed
console.log(data.posts.length); // number - guaranteed
console.log(data.posts[0].title); // string - with autocomplete
// Compile error if you access non-existent property
console.log(data.username); // Error: Property 'username' does not exist
return data;
}Union Types and Discriminated Unions
// TypeScript's most powerful feature: discriminated unions
type ApiResponse<T> =
| { status: "success"; data: T }
| { status: "error"; message: string; code: number }
| { status: "loading" };
function handleResponse(response: ApiResponse<User>) {
switch (response.status) {
case "success":
// TypeScript knows 'data' exists here
console.log(response.data.name);
break;
case "error":
// TypeScript knows 'message' and 'code' exist here
console.error(`Error ${response.code}: ${response.message}`);
break;
case "loading":
// TypeScript knows nothing else is available
console.log("Loading...");
break;
}
}
// More practical examples
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
}
}Generics
// Generics: write once, use with any type
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
const num = firstElement([1, 2, 3]); // type: number | undefined
const str = firstElement(["a", "b", "c"]); // type: string | undefined
// Generic with constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 30, email: "alice@example.com" };
const name = getProperty(user, "name"); // type: string
const age = getProperty(user, "age"); // type: number
// getProperty(user, "phone"); // Error: "phone" is not a key of user
// Generic API client
async function apiGet<T>(url: string): Promise<T> {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<T>;
}
// Usage - fully typed responses
const users = await apiGet<User[]>("/api/users");
const post = await apiGet<Post>("/api/posts/1");Type Safety Benefits in Practice
Catching Bugs at Compile Time
// Bug 1: Typos in property names
interface Config {
apiUrl: string;
timeout: number;
retries: number;
}
const config: Config = {
apiUrl: "https://api.example.com",
timout: 3000, // Compile error! Did you mean 'timeout'?
retries: 3
};
// Bug 2: Missing null checks
function getLength(str: string | null): number {
return str.length; // Error: 'str' is possibly 'null'
return str?.length ?? 0; // Correct: handle null case
}
// Bug 3: Exhaustiveness checking
type Status = "active" | "inactive" | "pending" | "archived";
function getStatusLabel(status: Status): string {
switch (status) {
case "active": return "Active";
case "inactive": return "Inactive";
case "pending": return "Pending";
// If you forget "archived", TypeScript warns:
// Error: Not all code paths return a value
}
}
// Bug 4: Wrong function arguments
function sendEmail(to: string, subject: string, body: string): void {
// ...
}
// JavaScript: silently swaps arguments
sendEmail("Hello!", "alice@example.com", "Welcome");
// TypeScript: you'd notice the logical error through IDE hintsWhen to Use JavaScript
- Quick prototyping and scripts: One-off scripts, automation tasks, and rapid prototypes where type safety slows you down
- Small projects (under 500 lines): The overhead of TypeScript configuration is not worth it for tiny projects
- Learning and experimentation: Beginners should learn JavaScript first before adding types
- Simple server-side scripts: Node.js CLI tools, cron jobs, and simple API endpoints
- Legacy codebases: When migrating would be too costly and the codebase is stable
- Team lacks TypeScript experience: Forcing TypeScript on an unfamiliar team creates friction
- Content-heavy sites: Static sites, blogs, and marketing pages with minimal logic
When to Use TypeScript
- Team projects (2+ developers): Types serve as documentation and prevent integration bugs
- Long-lived codebases: Code that will be maintained for months or years benefits enormously from types
- Complex business logic: Financial calculations, state machines, and domain models need type safety
- API-heavy applications: Typed API clients catch mismatches between frontend and backend
- React/Vue/Angular projects: Component props, state, and events are much safer with types
- Library and SDK development: Published packages with TypeScript types provide better developer experience
- Refactoring: The compiler catches every place affected by a change
- Enterprise applications: Compliance, auditing, and code quality standards favor static typing
Migration: JavaScript to TypeScript
You do not need to migrate all at once. TypeScript supports incremental adoption -- you can convert files one at a time.
// tsconfig.json for gradual migration
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": false, // Start with strict off
"allowJs": true, // Allow .js files alongside .ts
"checkJs": false, // Don't type-check .js files yet
"outDir": "./dist",
"rootDir": "./src",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}# Step-by-step migration plan:
# 1. Install TypeScript
npm install -D typescript @types/node
# 2. Generate tsconfig.json
npx tsc --init
# 3. Rename files one at a time: .js -> .ts
# Start with utility files, then move to core business logic
# 4. Fix type errors as you go
# Use 'any' temporarily for complex types
# 5. Gradually enable strict options:
# "noImplicitAny": true
# "strictNullChecks": true
# "strict": true
# 6. Add type definitions for dependencies
npm install -D @types/express @types/lodashMigration Cheat Sheet: Common Patterns
// Pattern 1: Function parameters
// Before (JS): function add(a, b) { return a + b; }
// After (TS):
function add(a: number, b: number): number { return a + b; }
// Pattern 2: Object parameters
// Before (JS): function createUser(options) { ... }
// After (TS):
interface CreateUserOptions {
name: string;
email: string;
role?: "admin" | "user"; // optional with union type
}
function createUser(options: CreateUserOptions): User { /* ... */ }
// Pattern 3: Array methods
// Before (JS): const names = users.map(u => u.name)
// After (TS):
const names: string[] = users.map((u: User) => u.name);
// Pattern 4: Event handlers (React)
// Before (JS): function handleClick(e) { ... }
// After (TS):
function handleClick(e: React.MouseEvent<HTMLButtonElement>): void { /* ... */ }
// Pattern 5: Async functions
// Before (JS): async function fetchData() { ... }
// After (TS):
async function fetchData(): Promise<ApiResponse<User[]>> { /* ... */ }
// Pattern 6: When you don't know the type yet, use unknown instead of any
function parseJSON(text: string): unknown {
return JSON.parse(text);
}
// Then narrow the type
const result = parseJSON('{"name": "Alice"}');
if (typeof result === 'object' && result !== null && 'name' in result) {
console.log((result as { name: string }).name);
}Performance: TypeScript vs JavaScript
| Aspect | JavaScript | TypeScript |
|---|---|---|
| Runtime performance | Identical | Identical (compiles to JS) |
| Build time | None (or minimal bundling) | Adds compilation step |
| Bundle size | Baseline | Same (types are removed) |
| IDE responsiveness | Fast | Can be slower on large projects |
| CI/CD pipeline | Faster | Adds type-check step |
| Cold start (serverless) | Identical | Identical (runtime is JS) |
TypeScript has zero runtime cost. All type annotations are removed during compilation. The compiled JavaScript output is the same code you would write by hand. Build time overhead is usually 1-5 seconds with modern tools like esbuild or SWC.
TypeScript Strictness Levels
Level 1 (Minimal): allowJs + no strict
â Just rename .js to .ts, minimal changes
â Good for initial migration
Level 2 (Moderate): noImplicitAny
â All function params need types
â Variables can still be inferred
Level 3 (Recommended): strict: true
â Enables all strict checks:
- noImplicitAny
- strictNullChecks
- strictFunctionTypes
- strictBindCallApply
- strictPropertyInitialization
- noImplicitThis
- alwaysStrict
â Catches the most bugs
Level 4 (Maximum): strict + additional rules
â noUncheckedIndexedAccess
â noPropertyAccessFromIndexSignature
â exactOptionalPropertyTypes
â Catches edge casesEcosystem and Tooling
| Tool | JavaScript Support | TypeScript Support |
|---|---|---|
| VS Code | Excellent | Exceptional (built for TS) |
| Next.js | Full support | First-class support |
| React | Full support | First-class with @types/react |
| Node.js | Native | Via tsc, tsx, ts-node |
| Deno | Full support | Native (no build step) |
| Bun | Full support | Native (no build step) |
| ESLint | Full support | Full support (typescript-eslint) |
| Jest | Native | Via ts-jest or SWC |
| Vite | Full support | Full support (esbuild) |
Decision Framework
| Question | If Yes: Use |
|---|---|
| Is this a 100-line script? | JavaScript |
| Will multiple developers work on it? | TypeScript |
| Does it have complex data structures? | TypeScript |
| Is it a quick prototype to test an idea? | JavaScript |
| Will it be maintained for over 6 months? | TypeScript |
| Does the team already know TypeScript? | TypeScript |
| Are you building a published library/SDK? | TypeScript |
| Is it a static site with minimal logic? | JavaScript |
Conclusion
TypeScript and JavaScript are not competitors -- TypeScript is JavaScript with an extra safety layer. For solo developers working on small projects, JavaScript's simplicity is a real advantage. For teams building applications that will grow and evolve over time, TypeScript's compile-time checks, IDE support, and self-documenting types prevent entire categories of bugs.
The 2026 trend is clear: the majority of new professional projects start with TypeScript. But that does not mean JavaScript is obsolete -- it remains the runtime language, and understanding JavaScript deeply is essential for effective TypeScript development. Learn JavaScript first, adopt TypeScript when complexity demands it, and always prioritize writing clear, maintainable code regardless of which you choose.
Convert between TypeScript and JavaScript with our TypeScript to JavaScript Converter, explore TypeScript Generics Explained, or dive into TypeScript Utility Types for advanced patterns.