TypeScript doesn't replace JavaScript. It's JavaScript plus a way to say what kind of value each variable, parameter and return value should hold — and a program, the type checker, that reads those annotations and warns you when the code breaks them.
The single most important thing to understand about it: the types exist only before your code runs.
Annotations, checked, then erased
TypeScript is JavaScript plus type annotations. The annotations are read by one program — the type checker — before your code ever runs.
function total(prices: number[]): number {
return prices.reduce((sum, p) => sum + p, 0);
}
total([4, 6]); // fine
total(['4', 6]); // error: Type 'string' is not assignable to type 'number'.
Plain JavaScript would happily run that second call and return "046" —
string concatenation — and you'd find out from a bug report. TypeScript
flags it in your editor as you type.
Then, to run the code, the compiler strips every annotation and emits ordinary JavaScript:
function total(prices) {
return prices.reduce((sum, p) => sum + p, 0);
}
That's what the browser or Node actually executes. There is no number[]
at runtime and no type check when the function is called.
Inference: you write fewer types than you think
TypeScript works out most types on its own:
const count = 3; // number (actually the literal type 3)
const names = ['Ada', 'Grace']; // string[]
const user = { id: 1, name: 'Ada' }; // { id: number; name: string }
names.push(42); // error: Argument of type 'number' is not assignable to parameter of type 'string'.
The common style is: annotate function parameters and public APIs, and let inference handle local variables.
The basic types
let title: string = 'Hello';
let total: number = 9.99;
let done: boolean = false;
let tags: string[] = ['a', 'b'];
let pair: [string, number] = ['age', 36]; // a tuple
let id: string | number = 42; // a union: either one
let maybe: string | undefined; // might not be set
type User = { // a named object shape
id: number;
name: string;
email?: string; // optional
};
type and interface both describe object shapes; for everyday use they're
interchangeable.
What TypeScript catches
- Typos in property names:
user.nmae. - Calling something that might be
undefinedornull. - Passing the wrong kind of argument.
- Forgetting to handle a case (with discriminated unions).
- Breaking callers when you change a function's signature — the checker lists every place that needs updating.
That last one is the real payoff on a large codebase: refactoring stops being scary.
What it can't catch: the outside world
Because types are erased, TypeScript can't check data that arrives at
runtime — an API response, a form, localStorage, a file.
type User = { id: number; name: string };
const res = await fetch('/api/user');
const user = (await res.json()) as User; // "trust me"
user.name.toUpperCase(); // crashes if the API sent { username: … }
as User doesn't convert or check anything; it tells the compiler to
believe you. If the data doesn't match, you get the same runtime crash
plain JavaScript would give you.
The fix is to validate at the boundary: check the data's shape when it
enters your program, with a few typeof checks or a schema library like
Zod, and let the types do their job from there inward.
Escape hatches to avoid
anyturns off checking for a value — and everything it touches. Preferunknown, which forces you to check before using it.ascasts silence errors rather than fixing them.// @ts-ignorehides an error on the next line.
Each has legitimate uses, but each one is a place where the types are lying, and the safety net has a hole.
Getting started
- Rename a
.jsfile to.tsand fix what the checker reports — you can adopt TypeScript one file at a time. - Turn on
"strict": trueintsconfig.jsonfrom the start. It enables the null checks that catch the most bugs. - Let your editor show you inferred types by hovering — it's the fastest way to learn what TypeScript knows.
TypeScript is a very fast, very thorough code reviewer that reads every line before you run it — and then gets out of the way completely.