To learn TypeScript is to keep writing JavaScript while telling the compiler, in advance, what shape your data has. That’s the whole idea. Everything else — generics, utility types, satisfies, conditional types — is extension, not foundation.
This article is built differently from most tutorials. There’s no “become a TypeScript pro in 7 days” schedule here, because that schedule doesn’t exist. Instead you get a roadmap with honest hour estimates, the compiler’s actual error messages quoted verbatim, and numbers we measured on our own server.
Measurement date: 4 August 2026
Every version and timing figure below was queried live on that day, not recalled from memory. That matters more than usual right now, because TypeScript has just been through the largest rebuild in its history — and a great deal of the tutorial content online still describes a world that no longer exists.
Here’s what we measured:
| Item | Value | How we checked |
|---|---|---|
| Current stable release | 7.0.2 | npm view typescript dist-tags |
| Published | 8 July 2026 | npm view typescript time --json |
| Previous generation | 6.0.3 (16 April 2026) | same |
| Release candidate for 7 | 7.0.1-rc (18 June 2026) | same |
| Repo of the new compiler | microsoft/typescript-go, 26,136 stars | GitHub API |
| Classic repo | microsoft/TypeScript, 110,061 stars, latest release tag: v6.0.3 | GitHub API |
Verify it yourself — it takes five seconds:
npm view typescript dist-tags
npm view typescript version
If that prints something other than 7.0.2, this article is stale on that point and you already have the correct answer.
The short version
- TypeScript is JavaScript plus types. Every valid
.jsfile is already valid TypeScript. You are not starting from zero. - Types do not exist at runtime. They’re stripped before execution. TypeScript checks you as you write; it does not guard you as you run.
- Realistic time to productive: roughly 8 to 15 hours for a working JavaScript developer, 3 to 6 months to fluency. Absolute beginners should learn JavaScript first.
- TypeScript 7 is a complete rewrite in Go. We measured 0.62 s vs 2.34 s on 1,500 files — a 3.8× factor in our test, with 40 % less memory.
- The npm package no longer ships a JavaScript library. It ships a native binary. Irrelevant if you’re learning; breaking if you build tooling.
tsc --initnow produces much stricter defaults — ordinary beginner code fails immediately. We show exactly why that’s a good thing.- The honest part: some projects should not use TypeScript. We name them below.
Follow along without installing anything Every example in this article runs in our TypeScript Playground: code on the left, type errors and compiled JavaScript on the right. Everything stays in your browser.
Who this is for — and who it isn’t
Two audiences with genuinely different problems. Here’s the split, so you don’t read the wrong half.
Group A: you know JavaScript and want to switch
You’ve written JS for months or years. You know map, async/await, modules, promises. Your problem isn’t programming — it’s a new notation layer. You have to learn to talk about the shape of your data rather than only its processing.
Your ramp is pleasantly short. Skip the “what even is a type” material and start at “The six concepts that cover 90 %”. Realistically, one focused weekend makes you productive. Not fluent — productive.
Group B: you’re a genuine beginner
You’ve barely programmed and keep hearing TypeScript is “the standard”. That’s true. Here’s the uncomfortable advice anyway:
Learn JavaScript first. At least the fundamentals.
This isn’t gatekeeping, it’s diagnostics. Learn TypeScript without JavaScript and every error message describes two problems at once: a flaw in your program’s logic and a type violation. You can’t separate them, because you have no baseline. That’s the single most common reason beginners experience TypeScript as hostile.
A workable path for Group B:
- JavaScript fundamentals — variables, functions, arrays, objects, loops,
fetch. Around 40 to 80 hours across a few weeks. - Finish one small project in plain JS. Don’t skip this. You need the lived experience of code working without types, or you’ll never understand what types add.
- Then come back here, from the top.
Skip steps 1 and 2 and you fight on two fronts later. We’ve seen it often enough to say it plainly.
What TypeScript actually is (and isn’t)
TypeScript is a superset of JavaScript. Concretely: rename app.js to app.ts and the file is valid TypeScript. Nothing breaks, nothing needs rewriting. You can then start adding types — file by file, line by line.
The second point is the one beginners most often misread:
There are no types at runtime. The compiler checks your code and then throws the annotations away. What executes is ordinary JavaScript. That has an immediate consequence:
function process(data: string) {
return data.toUpperCase();
}
// Compiles fine — the compiler knows the type.
process("hello");
// If this number arrives at runtime from an API,
// the compiler never saw it. It still crashes.
const fromApi: any = 42;
process(fromApi);
TypeScript protects you while writing, not while running. Data from outside — API responses, form input, JSON.parse, localStorage — is a blind spot. Understanding this early saves you a whole category of disappointment. How to defend that boundary is covered below, under unknown.
Node now runs TypeScript directly — but it checks nothing
This is new and currently causing a lot of confusion. We measured it on Node v22.23.0:
$ node hello.ts
node ran TS directly: 5
That works. Node strips the types and runs the rest. Now the part that matters — the same execution with an obvious type error:
const x: number = "not a number";
console.log("node did NOT complain:", x);
$ node nativebad.ts
node did NOT complain: not a number
Node didn’t catch the error because Node doesn’t check. It performs type stripping and nothing else. Checking is exclusively tsc’s job. Anyone who believes node file.ts means “running TypeScript” has silently disabled type safety without noticing — the most dangerous state there is, because everything looks green.
We hit a second limitation in the same test. Some TypeScript constructs can’t simply be erased:
$ node enum.ts
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode
enum emits real runtime code, so it’s forbidden in strip mode. That’s exactly what the erasableSyntaxOnly compiler option is for — it rejects such constructs at check time:
$ tsc --erasableSyntaxOnly enum.ts
enum.ts(1,6): error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
Practical rule: if you want node file.ts, enable erasableSyntaxOnly and avoid enum (use string literal unions instead — more on those below). And keep running tsc --noEmit anyway. Node doesn’t replace the compiler; it replaces the build step.
TypeScript 7: why your tutorials are going stale
TypeScript 7.0.2 shipped as stable on 8 July 2026. This isn’t an ordinary major version — it’s a complete rewrite of the compiler in Go. The previous compiler was itself written in TypeScript and ran on Node.
For learners this is good news and otherwise invisible: the language is unchanged, your types look identical, tsc is still tsc. But there are details worth knowing, because they explain why some guides no longer apply.
What we measured ourselves
We installed both versions on the same machine (AMD EPYC, 12 cores) and compiled identical code. The test code was generated: modules with interfaces, mapped types, keyof, Map, and an import chain where each module consumes the previous one — real dependency work for the checker, not a trivial pile of independent files.
Results, median of three runs each, --noEmit --strict:
| Scale | TypeScript 6.0.3 | TypeScript 7.0.2 | Factor |
|---|---|---|---|
| 1 file (4 lines) | 1.06 s | 0.28 s | 3.8× |
| 300 files (~9,000 lines) | 1.41 s | 0.50 s | 2.8× |
| 1,500 files (~30,000 lines) | 2.34 s | 0.62 s | 3.8× |
And peak process memory on the largest run:
| TypeScript 6.0.3 | TypeScript 7.0.2 | |
|---|---|---|
| 1,500 files | 316 MB | 192 MB |
Two observations surprised us, and we consider them more useful than the headline factor:
First: the improvement on the smallest project is proportionally as large as on the biggest. So the win isn’t only about scaling — a substantial part of it is startup. TypeScript 6 must load 8.9 MB of JavaScript and let Node parse it before it looks at your first line. For day-to-day work that’s the more important number: it’s not the giant build that gets faster, it’s the hundred small checks you run per day.
Second: going from 300 to 1,500 files — five times the code — moved TypeScript 7 only from 0.50 s to 0.62 s. Fixed overhead dominates here too. We therefore would not claim that every real project gets exactly 3.8× faster. Generated code with simple types is forgiving; real projects with deep generics, large node_modules and complex library types stress the checker differently. We report what we measured, not what we’d like to extrapolate.
The npm package no longer contains a JavaScript library
This is the break you won’t notice while learning and will notice instantly while building tools. We looked inside both packages:
# TypeScript 6.0.3
$ ls -la node_modules/typescript/lib/typescript.js
-rw-r--r-- 1 root root 9144216 node_modules/typescript/lib/typescript.js # 8.9 MB
# TypeScript 7.0.2
$ ls node_modules/typescript/lib/typescript.js
ls: cannot access '...': No such file or directory
The file simply isn’t there. Instead, the package pulls in a platform-specific binary:
$ du -sh node_modules/@typescript/typescript-linux-x64
27M node_modules/@typescript/typescript-linux-x64
$ file node_modules/@typescript/typescript-linux-x64/lib/tsc
ELF 64-bit LSB executable, x86-64, statically linked, Go BuildID=..., stripped
A statically linked Go program. The typescript package declares twenty such platform packages as optional dependencies; npm fetches the one matching your system.
The consequence for programs that use the compiler as a library:
# TypeScript 6.0.3
$ node -e "const ts=require('typescript'); console.log(ts.version, typeof ts.createProgram)"
6.0.3 function
# TypeScript 7.0.2
$ node -e "const ts=require('typescript'); console.log(ts.version, typeof ts.createProgram)"
7.0.2 undefined
Under version 7, require('typescript') returns only version and versionMajorMinor — nothing else. The old compiler API is gone from the main export. There is a successor, and it wears its maturity in its name:
$ node -e "import('typescript/unstable/sync').then(a=>console.log(Object.keys(a).length,'exports'))"
43 exports
The paths are typescript/unstable/sync, typescript/unstable/ast, typescript/unstable/async and so on. “unstable” there is a statement of fact, not modesty.
What does this mean for you as a learner? If you merely use TypeScript: nothing at all. It becomes relevant when something in your toolchain talks to the compiler internally — older ESLint setups, documentation generators, bundler plugins, codemod tools. If a tool dies with a strange message after you upgrade to 7, this is almost always the cause. The calm approach for existing projects is to stay on 6.0.3 until your toolchain catches up. For a new learning project there’s no reason not to take version 7 today.
A mistake we walked straight into
On our first attempt to check individual test files, every file produced only this:
$ tsc --noEmit --strict errs/e1.ts
error TS5112: tsconfig.json is present but will not be loaded if files are specified
on commandline. Use '--ignoreConfig' to skip this error.
No type checking, just that complaint. The underlying rule is old: passing filenames to tsc makes it ignore your tsconfig.json. That was always true — the compiler simply used to stay quiet about it. Now it says so loudly and refuses to proceed until you choose.
The interesting part: as a control, we ran the identical command under 6.0.3 and got the identical message. So the error code already exists in the previous generation — we’d just never seen it, because we never invoke tsc with filenames. Which taught us something worth passing on: we nearly wrote this up as a TypeScript 7 novelty. Behaviour you encounter for the first time is not the same as new behaviour. The control run against the old version cost twenty seconds and prevented a false claim.
The practical takeaway: invoke tsc without filenames. It finds tsconfig.json on its own and checks the whole project the way your editor does. Single files on the command line are nearly always a sign that something else has gone wrong.
And a genuine failure: our tsc --watch didn’t react
This is the part we’d have most liked to omit, which is precisely why it’s the most valuable. Watch mode is supposed to re-check on every file change. Ours didn’t.
The procedure: start tsc --watch, edit a file to introduce an unmistakable error, wait. Result:
10:00:31 - Starting compilation in watch mode...
10:00:32 - Found 0 errors. Watching for file changes.
[file edited — then eight seconds of silence. Nothing.]
Before turning that into a claim about TypeScript, we ran a control — same directory, same machine, same minute, only with 6.0.3:
10:00:52 - Starting compilation in watch mode...
10:00:53 - Found 0 errors. Watching for file changes.
10:00:58 - File change detected. Starting incremental compilation...
src/a.ts(1,14): error TS2322: Type 'string' is not assignable to type 'number'.
10:00:58 - Found 1 error. Watching for file changes.
Version 6 noticed after five seconds. Version 7 never noticed. So it wasn’t our test setup, our editor, or our patience.
We then went a layer down and traced the system calls. The result was unambiguous — and different from what we’d assumed:
$ strace -f -e trace=fanotify_mark ... tsc --watch
# Every directory actually registered for watching:
"/tmp/wfinal/node_modules/@typescript/typescript-linux-x64/lib"
Our source directory src/ was never registered, not once. The only thing being watched was the compiler’s own library directory. All four registration calls returned success — so this wasn’t missing permissions or a filesystem lacking the interface. The compiler simply never asked to watch our code.
One detail fits that picture, and explains why we almost talked ourselves out of the finding: in one intermediate test, watch mode did fire — when a new file appeared in the directory. Edits to existing files went unnoticed; creating a new one did not. Partial successes like that are treacherous: had we run only that test, this section would read “works fine”.
A look at the project’s issue tracker showed we weren’t alone: issue #4795, “tsc --watch doesn’t recompile on file change”, opened 30 July 2026, open at the time of our measurement. The reporter describes exactly our observation, including the 6.0.3-works / 7.0.2-doesn’t comparison. There’s also a pending fix (#4661) addressing a related problem in Docker environments — but that one describes failing registration calls, and ours succeeded. Our case sits on an ordinary ext4 partition inside a KVM guest, not Docker.
What to take from this: if your tsc --watch goes quiet under version 7, it isn’t you. Test it deliberately before relying on it — introduce an error on purpose and see whether it gets reported. Until it’s fixed, the workarounds are: your editor’s inline diagnostics (which run through the language server and were unaffected for us), a tsc --noEmit before every commit, or simply 6.0.3 for daily development.
And the broader lesson, which outlives this bug: a tool reporting nothing is not the same as a tool finding nothing. Permanent green with never a single red run is not evidence of clean code — it can equally mean nobody is looking any more. If you rely on checking tools, break something on purpose occasionally and confirm somebody notices.
The roadmap, with honest timings
Now the main event. The estimates below are focused working hours — not calendar time, not “while a video plays in the background”. An hour a day roughly doubles the calendar days.
And the uncomfortable preamble: there is no state called “learned TypeScript”. There is “I can work with this”, which arrives sooner than course vendors imply. And there is “I understand what the compiler is telling me”, which arrives considerably later than those same vendors admit.
Stage 1 — Set up and see your first error (1–2 hours)
Goal: a project where tsc runs and shows you a genuine error.
mkdir my-ts-project && cd my-ts-project
npm init -y
npm install --save-dev typescript
npx tsc --init
That last command writes a tsconfig.json. What lands in it has changed, and it matters enough for beginners to print. These are the active settings from our run with 7.0.2:
{
"compilerOptions": {
"module": "nodenext",
"target": "esnext",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"verbatimModuleSyntax": true,
"isolatedModules": true,
"noUncheckedSideEffectImports": true,
"moduleDetection": "force",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"jsx": "react-jsx",
"skipLibCheck": true,
"types": []
}
}
Considerably stricter than the defaults of earlier years. And here comes the part that reliably frustrates beginners — we reproduced it. Perfectly ordinary, correct JavaScript:
const names = ["Ada", "Alan", "Grace"];
const first = names[0];
console.log(first.toUpperCase());
In a freshly tsc --init-ed project, the compiler says:
src/index.ts(3,13): error TS18048: 'first' is possibly 'undefined'.
Reasonable first reaction: “It’s obviously a string, the array is right there!” The compiler is nonetheless correct, for a reason you only need to grasp once: names[0] is not a guarantee. On an empty array that expression yields undefined, and JavaScript raises nothing — you just get undefined and crash one line later. That’s noUncheckedIndexedAccess, and it catches one of the most common failure modes in the language.
The clean fix is one line:
const names = ["Ada", "Alan", "Grace"];
const first = names[0];
if (first !== undefined) {
console.log(first.toUpperCase());
}
// or, more compactly, with a fallback:
console.log((names[0] ?? "unknown").toUpperCase());
Our advice for Stage 1: leave the strict defaults on. It’s tempting to disable noUncheckedIndexedAccess the moment it nags you. But then you’re learning a weaker language and will have to catch up later, under deadline. The pain is cheapest at the start.
If you’d rather skip project setup and just try syntax: the TypeScript Playground runs in your browser — no install, no tsconfig.json. For first experiments with types it’s the fastest route: write on the left, see the result on the right.
Stage 2 — The six concepts that cover 90 % (4–8 hours)
This is where the leverage is. TypeScript has an enormous type system, but daily work uses the same small subset over and over.
1. Primitive annotations. The foundation, learned in two minutes:
let age: number = 34;
let name: string = "Ada";
let active: boolean = true;
let flags: string[] = ["new", "important"];
let something: unknown = fetchData();
Important: most of the time you don’t need to write these. TypeScript infers:
let age = 34; // automatically number
const name = "Ada"; // automatically the literal type "Ada"
A common beginner habit is annotating every variable. That inflates code without adding safety. Annotate function boundaries — parameters and return values. Let inference handle the rest.
2. Interfaces and type aliases — the shape of your data.
interface User {
id: number;
name: string;
email?: string; // optional — may be absent
readonly createdAt: Date; // can't be reassigned after construction
}
function greet(u: User): string {
return `Hello, ${u.name}`;
}
type does the same and more:
type User = { id: number; name: string };
type Id = string | number; // union — one or the other
type Timestamped = User & { updatedAt: Date }; // intersection
Which when? Honest answer: in daily work it barely matters. Use interface for object shapes others might extend; type for everything else, especially unions. When unsure, reach for type — it does everything interface does, plus unions.
3. Union types and narrowing — the highest-return concept in the language.
A union says “one of these”. Narrowing is what the compiler does once you convince it which one you currently have:
function length(x: string | string[]): number {
if (typeof x === "string") {
return x.length; // compiler knows: string
}
return x.length; // and here: string[]
}
You write an ordinary if, exactly as you would in JavaScript. The compiler reads along. This is the moment TypeScript clicks for most people: you don’t have to prove anything to the compiler, you just have to write normal defensive code, and it understands you.
String literal unions are especially useful. They replace enum entirely, work with node file.ts, and the error message is unbeatable:
type Status = "open" | "done";
const s: Status = "Open";
error TS2820: Type '"Open"' is not assignable to type 'Status'. Did you mean '"open"'?
The compiler spotted the typo and suggested the correct value. That’s the literal output from our test run, not a paraphrase.
4. Typing functions properly.
function add(a: number, b: number): number {
return a + b;
}
const double = (x: number): number => x * 2;
// Optional parameters and defaults
function greet(name: string, greeting: string = "Hello"): string {
return `${greeting}, ${name}`;
}
// Function as a parameter
function apply(values: number[], transform: (n: number) => number): number[] {
return values.map(transform);
}
Write return types or not? The compiler infers them, so you don’t have to. We still recommend it for anything that leaves a file — on an exported function, the return type is a promise to every caller. Without the annotation, a small change to the body can silently alter that contract, and the error then surfaces somewhere far away.
5. Generics — later than you think.
Generics are types with a placeholder. You need them far less often than tutorials suggest, but when you do they’re irreplaceable:
function firstItem<T>(arr: T[]): T | undefined {
return arr[0];
}
const a = firstItem([1, 2, 3]); // number | undefined
const b = firstItem(["x", "y"]); // string | undefined
The <T> says: “I don’t know what’s inside, but whatever goes in comes out.” Without generics you’d rewrite this per type, or reach for any and surrender all safety.
Our advice: use generics from day one — they’re inside Array<T>, Promise<T>, Map<K, V>. Write your own only after you’ve copied the same function a third time for a different type. Before that, it’s nearly always premature abstraction.
6. unknown instead of any — the single most valuable habit.
any switches type checking off. It’s an escape hatch and it earns its place — but every any is a hole in the net.
const data: unknown = JSON.parse('{"a":1}');
console.log(data.a);
error TS18046: 'data' is of type 'unknown'.
The compiler forces you to look before you leap. That’s the entire point:
const data: unknown = JSON.parse(rawText);
if (typeof data === "object" && data !== null && "a" in data) {
console.log(data.a); // now permitted
}
More typing, and correct in exactly the place where your data comes from outside. Incidentally, JSON.parse returns any by default — one of the few places where the standard library quietly removes your safety. Writing unknown explicitly takes it back.
Stage 3 — Learn to read compiler errors (2–4 hours, spread out)
This is the skill that separates people, and almost no tutorial teaches it. So here are the errors you’ll actually meet in your first weeks — all quoted verbatim from our test runs with TypeScript 7.0.2, not from memory:
| Code | Wording | What it actually means |
|---|---|---|
| TS2322 | Type 'string' is not assignable to type 'number'. | Wrong kind of value going into a variable. The classic first error. |
| TS2345 | Argument of type 'string' is not assignable to parameter of type 'number'. | Same thing, at a call site. Check your argument order. |
| TS2339 | Property 'email' does not exist on type '{ name: string; }'. | You’re reading a field the type doesn’t have. Usually a typo or an over-narrow type. |
| TS18048 | 'n' is possibly 'undefined'. | The value might be missing. Check first, or use ??. |
| TS18047 | 'el' is possibly 'null'. | Nearly always document.getElementById, which returns null when nothing matches. |
| TS2820 | Type '"Open"' is not assignable to type 'Status'. Did you mean '"open"'? | Typo in a literal union. The compiler hands you the answer. |
| TS18046 | 'data' is of type 'unknown'. | Narrow the type before using it. |
| TS2741 | Property 'y' is missing in type '{ x: number; }' but required in type 'Point'. | A required field is absent. |
| TS7006 | Parameter 'a' implicitly has an 'any' type. | Under strict, parameters need types. The typical first error when renaming .js to .ts. |
Two reading techniques that save real time:
First: read from the bottom up. In nested diagnostics, the actual cause sits at the bottom, in the most deeply indented line. From our test:
error TS2375: Type '{ host: string; port: undefined; }' is not assignable to type 'Config'
with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of
the target's properties.
Types of property 'port' are incompatible.
Type 'undefined' is not assignable to type 'number'.
The first line is context. The last is the diagnosis: port is undefined but should be number.
Second: the error number is a far better search term than the text. TS2345 finds targeted explanations; the message text contains your own type names and therefore returns nothing useful.
Stage 4 — Build something real (10–40 hours)
Beyond this point, no tutorial helps — only work on something you care about. Build something that talks to an API, because that’s where the most important lesson lives: the boundary between the typed and untyped worlds.
interface WeatherResponse {
temperature: number;
city: string;
}
// Naive — and quietly dangerous:
async function getWeatherNaive(city: string): Promise<WeatherResponse> {
const res = await fetch(`https://api.example.com/weather?city=${city}`);
return res.json() as WeatherResponse; // an ASSERTION, not a check
}
That as is a promise to the compiler, not a verification. If the API changes shape, your code still compiles perfectly and crashes at runtime. The honest version checks:
function isWeatherResponse(x: unknown): x is WeatherResponse {
return (
typeof x === "object" && x !== null &&
"temperature" in x && typeof (x as Record<string, unknown>).temperature === "number" &&
"city" in x && typeof (x as Record<string, unknown>).city === "string"
);
}
async function getWeather(city: string): Promise<WeatherResponse> {
const res = await fetch(`https://api.example.com/weather?city=${city}`);
const raw: unknown = await res.json();
if (!isWeatherResponse(raw)) {
throw new Error("Unexpected response shape from weather API");
}
return raw; // guaranteed well-formed from here
}
The x is WeatherResponse is a type guard: a function whose return value tells the compiler something about a type. After the if, it knows. In larger projects, libraries like Zod or Valibot do this work — but write it by hand once, or the principle stays abstract.
Honest timing: a small but real project costs 10 to 40 hours depending on scope. After that you’re productive. You’ll still look things up — we haven’t stopped after years.
Stage 5 — Fluency (3–6 months alongside real work)
Utility types (Partial, Pick, Omit, Record, ReturnType), conditional types, template literal types, infer, declaration files for untyped libraries. These arrive through work, not study.
One concept from this stage is worth pulling forward, because it’s useful early — satisfies:
const routes = {
home: "/",
blog: "/blog",
} satisfies Record<string, `/${string}`>;
const h: string = routes.home; // keeps the precise literal type "/"
satisfies checks that your object conforms to a type without widening the inferred type. Write const routes: Record<string, string> and you lose the knowledge of which keys exist. With satisfies you keep it and still get the check. For configuration objects, it’s the right tool.
What you should NOT do here: type acrobatics for their own sake. There are impressive types circulating online — multi-level conditional types with recursive infer. Rule of thumb: if your type is harder to read than the code it protects, it has missed its purpose. Types are documentation that gets verified. Unreadable documentation isn’t documentation.
Migrating an existing JavaScript project
The most common real-world situation: code already exists and can’t stop moving. The mistake nearly everyone makes is the big-bang conversion over a weekend. That produces hundreds of simultaneous errors and nobody can tell which are real.
The approach that works is incremental.
Step 1: add TypeScript without renaming anything.
npm install --save-dev typescript
npx tsc --init
Then set allowJs in tsconfig.json and leave strict off for now. You’re compiling your JavaScript with the TypeScript compiler. It barely checks anything yet — that’s the point. You’re proving the toolchain runs before you tighten any rules.
Step 2: turn on checkJs and brace yourself.
Now the compiler also checks .js files. We tried it on typical legacy code:
function sum(a, b) { return a + b; }
const total = sum(1, "2");
migr/legacy.js(1,16): error TS7006: Parameter 'a' implicitly has an 'any' type.
migr/legacy.js(1,19): error TS7006: Parameter 'b' implicitly has an 'any' type.
What’s remarkable is what the compiler does not report: sum(1, "2") — the actual bug, a number plus a string. It can’t see it, because untyped parameters permit everything. No types, no checking — and a check that never happened looks exactly like a check that passed. That’s the entire case for TypeScript, in two lines of code.
Step 3: JSDoc — types without renaming files.
Underappreciated and very handy for migrations: you can type plain .js files using comments.
/**
* @param {number} a
* @param {number} b
* @returns {number}
*/
function sum(a, b) { return a + b; }
sum(1, "2");
migr/jsdoc.js(7,10): error TS2345: Argument of type 'string' is not assignable
to parameter of type 'number'.
Now it finds the real bug — in a .js file, with no build step and no renaming. For projects where conversion is politically or technically awkward, this is your foot in the door. Svelte famously ran on JSDoc rather than .ts for exactly this reason.
Step 4: rename file by file. Start at the leaves of your dependency tree — helpers, constants, pure data models. They import little and are imported by many, so every type you win radiates upward. Central, heavily-connected files go last.
Step 5: enable strict once enough has moved. Not at the start, or you’ll drown in TS7006. When you do enable it, go one flag at a time (noImplicitAny, then strictNullChecks) rather than all at once.
Realistic timing: for a mid-sized project of 20,000–50,000 lines, several weeks alongside normal work is realistic — not full-time, but spread out. Anyone promising “an afternoon” either has a very small project or has been generous with any.
The strict flags — what they cost and what they buy
Since tsc --init now enables several strict switches, here are the three you’ll trip over, each with its real error text.
noUncheckedIndexedAccess
Every indexed access also yields undefined.
const names: string[] = ["Ada", "Alan"];
const first: string = names[0];
error TS2322: Type 'string | undefined' is not assignable to type 'string'.
Type 'undefined' is not assignable to type 'string'.
Cost: more guard clauses. Benefit: you catch the class of bug that shows up in production as “Cannot read properties of undefined”. Our recommendation: leave it on.
exactOptionalPropertyTypes
Distinguishes “field absent” from “field explicitly undefined”.
interface Config { host: string; port?: number }
const c: Config = { host: "localhost", port: undefined };
error TS2375: Type '{ host: string; port: undefined; }' is not assignable to type
'Config' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to
the types of the target's properties.
The distinction is subtle but real: { host: "x" } and { host: "x", port: undefined } behave differently under Object.keys, in, and JSON serialisation. Cost: occasional confusion. Benefit: no guesswork around config objects. Defensible to disable as a beginner — but know what you’re disabling.
verbatimModuleSyntax
The flag with the greatest capacity to surprise. We deliberately misused it to show what happens:
import { User } from "./vms1.js";
export const u: User = { id: 1 };
error TS1295: ECMAScript imports and exports cannot be written in a CommonJS file
under 'verbatimModuleSyntax'. ...
error TS1484: 'User' is a type and must be imported using a type-only import when
'verbatimModuleSyntax' is enabled.
Three errors from two innocuous lines. The fix for the second is one word:
import type { User } from "./vms1.js";
import type says: “this is only a type, drop the import when compiling.” Without it, tooling has to guess whether an import is needed at runtime — and eliminating that guess is precisely what verbatimModuleSyntax is for. It’s therefore a prerequisite for fast transpilers like esbuild and for node file.ts.
The first error (TS1295) has nothing to do with types: our test file lived in a folder whose package.json lacked "type": "module", so it counted as CommonJS and wasn’t allowed to use import. Get into the habit of putting "type": "module" in new projects — a large share of baffling module errors dissolve on the spot.
The honest part: when TypeScript is not worth it
We’ve said a lot of good things about TypeScript because a lot of good things are true. But there are cases where the cost exceeds the benefit. Omitting them would be selling, not advising.
A script under 200 lines that you run once. A maintenance script, a one-off data migration, a thirty-line cron job. Configuration and build overhead are out of all proportion. Write it in JavaScript or Python and move on. Types amortise over maintenance — no maintenance, no amortisation.
Prototypes that genuinely get thrown away. When you’re exploring whether an idea works at all, your data shapes change daily. Maintaining types that shift hourly is ballast. The catch: “we’ll rewrite it later” is one of our industry’s most reliable self-deceptions. If the prototype has a real chance of reaching production, this paragraph no longer applies.
A team that doesn’t want it. That’s a social reason, not a technical one, and it’s still decisive. Forcing TypeScript on a resistant team predictably produces any at every inconvenient spot, @ts-ignore above every error, and a type system that claims to protect the code while doing nothing of the sort. That’s worse than no TypeScript — because it manufactures a safety nobody is checking any more. Convince first, migrate second.
Highly dynamic code. Some problems — plugin systems assembling objects at runtime, generic data processing over unknown structures, heavily metaprogrammed libraries — can only be described with substantial type gymnastics. If you notice you’re spending more time on the type system than on the problem, that’s a signal. unknown at the boundary plus an honest runtime check is often the better answer than a virtuoso type.
You’re currently learning to program. Stated above, but it belongs here: learning two systems at once — programming logic and a type system — doesn’t double the difficulty, it multiplies it. Because for every error, you can’t tell which of the two worlds it came from.
And where TypeScript is overrated: it finds type errors. It finds no logic errors. A program that computes VAT incorrectly compiles flawlessly. So does a sort that runs ascending when you meant descending. TypeScript doesn’t replace tests, it complements them. We’ve watched teams cut test coverage after adopting it, with entirely predictable results.
Tools that ease the way in
Your editor is the most important tool. The language server shows errors as you type, long before you invoke tsc. And that path kept working for us even while watch mode was silent — one more reason to trust the editor’s diagnostics and additionally run the compiler before every commit.
A playground for experiments. When your question is “what happens if I…”, setting up a project is too slow. Our TypeScript Playground runs in the browser: code on the left, result on the right, no installation. For working through this article’s examples, it’s the fastest route.
AI assistants are unusually helpful with TypeScript — especially while learning, because error messages have a structure models resolve well. Instead of searching an error code, paste the message along with the offending line and ask for the cause. Which tools are worth using today is something we tested in our comparison of the best AI coding tools.
One warning we consider important: language models are trained on code from the past. With an upheaval like TypeScript 7, that means you will fairly reliably receive answers describing the version 5 world — including advice to use require('typescript'), which, as measured above, no longer works. Always verify version claims yourself. An npm view typescript version takes two seconds and is the one source that never goes stale.
For framework work: if you’re learning TypeScript alongside Next.js, the area generating the most type questions is routing — particularly since route params became asynchronous. We covered that in depth in our guide to Next.js dynamic routes; those examples make a good practical test of everything you’ve learned here about Promise<T> and narrowing.
For documenting your project: well-typed code deserves a readable README. Our Markdown generator handles the formatting when you publish your first TypeScript project.
Frequently asked questions
Do I need JavaScript before learning TypeScript? For Group B (true beginners): yes, at least the fundamentals. Not because of syntax, but because of diagnosis — you have to be able to tell a logic error from a type violation. Budget 40 to 80 hours of JavaScript first.
How long does learning TypeScript really take? For JS developers: 8 to 15 focused hours to productive use, 3 to 6 months alongside real work to fluency. For beginners, add the JavaScript time. Anything promising “a weekend” is describing syntax familiarity, not capability.
Should I start with TypeScript 6 or 7? For a new learning project: version 7 (currently 7.0.2 as of 4 August 2026). It’s faster and the language is identical. For an existing project with a mature toolchain, be careful — check whether your tools use the compiler API. And keep an eye on watch mode, as described above.
What’s the difference between interface and type?
In practice, almost none. interface can be extended by redeclaration (useful for library types); type additionally handles unions, intersections and conditional types. When in doubt, type.
Why does TypeScript complain about array[0]?
That’s noUncheckedIndexedAccess, a tsc --init default. The access can return undefined if the array is empty. Guard with if or use ?? fallback. Leave it enabled — it catches genuine production bugs.
Can I use TypeScript without a build step?
Partly. Node runs .ts files directly, but it only strips types and checks nothing. For checking you still need tsc --noEmit. Additionally, constructs like enum are forbidden in strip mode — erasableSyntaxOnly warns you up front.
Is any always bad?
No, but it’s always an exception. As a migration bridge it’s legitimate. As a permanent fixture it hollows out the type system. If you need an escape hatch, use unknown — it forces a check at the point of use instead of silently skipping one.
Does TypeScript slow my application down? No — not a single type artefact exists at runtime. What costs time is compilation during development, and that’s exactly what got 3.8× shorter in our measurements with version 7.
Conclusion
Learning TypeScript is less work than the language’s reputation suggests, and more work than course vendors promise. For JavaScript developers, productive entry is a matter of 8 to 15 hours; fluency arrives over months of application and can’t be shortcut.
Three things we’d send you off with:
One: learn to read error messages, not to memorise features. The Stage 2 concepts cover daily work. What actually separates you from a beginner is knowing, on seeing TS2345, exactly where to look.
Two: leave the strict defaults on. They’re annoying at the start and that’s precisely when they’re cheapest. noUncheckedIndexedAccess catches a bug that would otherwise visit you at three in the morning, in production.
Three: never trust a tool that merely stays quiet. Our watch mode reported “0 errors” for hours — while never once looking at our source directory. A green run only proves something once you’ve seen the same setup turn red. Break something on purpose from time to time. It’s the cheapest test there is, and the only one that tests your testing tools.
Every version and timing figure in this article was measured on 4 August 2026. Check the current state with npm view typescript version — and if it disagrees, believe the command line, not this article.
