TypeScript vs JavaScript

October 29, 20269 min read
Requirements:
ObjectsFunctions| Unions

TypeScript vs JavaScript is a slightly misleading comparison, because the two are not competing options. TypeScript is JavaScript with a type layer on top, and that layer is deleted before your code runs. Every .js file you have ever written is already valid TypeScript — rename it to .ts and the compiler takes it.

So the real question is not which language to pick. It is whether the type layer earns the build step it costs you.

This article covers what TypeScript actually adds, what you give up to get it, whether it replaces JavaScript, and which one to learn first.

TypeScript vs JavaScript at a glance

JavaScriptTypeScript
Runs in a browser or NodeDirectlyOnly after compiling to JS
When types are checkedAt runtime, if at allBefore the code ever runs
Build stepNot requiredRequired (tsc or a bundler)
File extensions.js, .mjs, .cjs.ts, .tsx, plus all of those
Runtime cost—None, types are erased
Editor autocompleteGuessed from usageDerived from the types
Standard librarySameSame

The last row matters more than people expect. There is no TypeScript Array, no TypeScript fetch, no TypeScript Promise. It is the same runtime, the same methods, the same behavior.

Every JavaScript file is already TypeScript

Here is a bug that JavaScript is happy to ship:

const rawPrice = JSON.parse('"20"') // a string, not a number
const withFee = rawPrice + 5
 
console.log(withFee) // "205"

Nothing throws. The + operator sees a string, concatenates, and you find out when a customer reports a $205 charge.

TypeScript does not change what that code does. It changes when you find out:

interface CartLine {
  price: number
  quantity: number
}
 
function lineTotal(line: CartLine): number {
  return line.price * line.quantity
}
 
lineTotal({ price: 20, quantity: 2 }) // 40
 
// lineTotal({ price: '20', quantity: 2 })
// Error: Type 'string' is not assignable to type 'number'.

That commented-out line is the whole value proposition. It is an error in your editor, a failed build in CI, and never a support ticket.

What TypeScript adds

Object shapes you can name

JavaScript objects have no declared shape. TypeScript lets you write one down with an interface:

interface UserProfile {
  id: string
  email: string
  plan: 'free' | 'pro'
  trialEndsAt?: Date
}
 
function profileBadge(user: UserProfile): string {
  return user.plan === 'pro' ? 'Pro member' : 'Free account'
}

Three things follow from those six lines. Autocomplete knows user. has four properties. A typo like user.emial is an error instead of undefined. And the ? on trialEndsAt forces you to handle the case where it is missing.

Functions that stay honest about types

Generics let one function work across types without falling back to any:

function firstItem<Item>(items: Item[]): Item | undefined {
  return items[0]
}
 
const firstPlan = firstItem(['free', 'pro']) // string | undefined
const firstScore = firstItem([10, 20]) // number | undefined

The return type tracks the input. Pass strings, get a string back. The | undefined is TypeScript pointing out that an empty array is possible — a check JavaScript leaves entirely to you. Generics are the main reason library authors move off plain JS.

Values that are one of several things

API responses are rarely a single shape, and union types model that directly:

type ApiResult =
  | { status: 'ok'; body: string }
  | { status: 'error'; code: number }
 
function describeResult(result: ApiResult): string {
  if (result.status === 'ok') {
    return `received ${result.body.length} characters`
  }
 
  return `failed with code ${result.code}`
}

Inside the if, TypeScript knows result has a body. After it, it knows there is a code. Reading result.code in the first branch is an error. That narrowing is pure JavaScript control flow — the compiler just follows along.

Syntax that is not JavaScript at all

Most of TypeScript disappears at compile time. Enums are the notable exception:

enum Tier {
  Free = 'free',
  Pro = 'pro',
}
 
function tierLabel(tier: Tier): string {
  return tier === Tier.Pro ? 'Pro' : 'Free'
}

enum is not a JavaScript keyword, and it does not vanish — TypeScript emits a real object into your bundle. It is the one feature that adds code rather than removing it, which is exactly why enums show up on so many "features to avoid" lists.

What TypeScript costs

Be honest about this part, because it is not free.

A build step. JavaScript runs as written. TypeScript has to be compiled first, which means a compiler in your dependencies and a command in your scripts:

npm install --save-dev typescript
npx tsc --init

That is covered in detail in npm typescript.

A config file. tsc --init creates a tsconfig.json, and the settings in it decide how strict the type checking is and what JavaScript comes out the other end:

{
  "compilerOptions": {
    "strict": true,
    "target": "ES2022",
    "module": "nodenext"
  }
}

Types for your dependencies. Modern packages ship their own. Older ones need a companion @types/ package, and a few have neither — then you write the types yourself or work around them. This is rarely a blocker in practice, but it is the kind of friction that does not exist at all in a plain JavaScript project, and it tends to show up on the day you are trying to ship something else.

A real learning curve. Generics, conditional types, and narrowing take time. There is an escape hatch — any turns checking off for a value — and leaning on it too early is how teams end up with TypeScript that catches nothing.

And one limit worth stating plainly: types are not runtime validation. The compiler checks your code, not your data.

const parsed: { email: string } = JSON.parse('{}')
 
console.log(parsed.email) // undefined, despite the type saying string

TypeScript believed the annotation. The network did not. For data crossing a boundary you still need a runtime check.

Moving an existing JavaScript project

The comparison usually comes up when someone is weighing a migration, and the good news is that it is not an all-or-nothing switch. TypeScript was designed to be adopted one file at a time.

Two compiler options do most of the work:

{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": false,
    "strict": false
  }
}

allowJs lets .js and .ts files import each other freely, so you can rename a single file, fix what the compiler complains about, and leave the other four hundred alone. Nothing else in the project needs to change that day.

Flipping checkJs to true is the interesting step. It type-checks your plain JavaScript files without renaming anything, using whatever the compiler can infer. On a mature codebase that usually surfaces a handful of genuine bugs on the first run — and it is the cheapest possible trial of the whole idea, because you can turn it back off.

You can also add types to JavaScript without leaving JavaScript, using JSDoc comments:

/** @type {Map<string, number>} */
const seatCounts = new Map()
 
seatCounts.set('pro', 12)

With checkJs on, the compiler honors that annotation and will flag seatCounts.set('pro', 'twelve') as an error — in a .js file, with no build step added. It is a legitimate end state for projects that want the checking without the toolchain.

Leave strict for last. Turning it on at the start of a migration buries you in errors from code that already works fine in production.

Does TypeScript replace JavaScript?

No, and it is not trying to. Browsers run JavaScript. Node runs JavaScript. The compiler's main job is to strip the types out and hand the engine ordinary JS — your interface declarations produce exactly zero bytes of output.

Recent Node versions blur the line by running .ts files directly, but that is the same erasure done at load time instead of in a build step. The types still never reach the runtime.

A useful way to hold it: TypeScript is a development tool that happens to look like a language. You ship JavaScript either way.

Which should you learn first?

JavaScript, and it is not close.

Closures, this, the event loop, prototypes, array methods, promises — all of that is JavaScript, and TypeScript assumes you know it. Learning both at once means every confusing moment has two possible causes, and beginners usually blame the wrong one.

A reasonable order:

  1. JavaScript fundamentals until you can read code without guessing.
  2. TypeScript basics — annotations, interfaces, unions.
  3. strict: true on a small project, so the compiler teaches you.
  4. Generics and the utility types, once you feel the need for them.

If you already write JavaScript professionally, skip to step two. Most of what makes TypeScript hard is the parts that have no JavaScript equivalent, and there are fewer of those than the tooling suggests.

The short answer

TypeScript vs JavaScript comes down to one trade: a build step and some syntax to learn, in exchange for catching a whole category of mistakes before anyone runs the code.

For a script you will delete tomorrow, that trade is not worth it. For anything with more than one contributor or more than a few months of life, it usually is — which is why the question these days is less "should we use TypeScript" and more "how strict should we set it".

Share this article

Become a TypeScript Pro

Track your progress through 200+ hands-on challenges. Free, sign in with GitHub.

Or start solving right away: explore all TypeScript challenges

Practice with Challenges

Put your typescript vs javascript knowledge to the test with these related challenges.

#4Pick
Easy
#3312Parameters
Easy

Related Concepts

Concepts that build on or relate to typescript vs javascript.

npm typescript: Install, Pin, Runtsconfig.json ExplainedInterfacesTypeScript GenericsUnion TypesTypeScript Enums