13. TypeScript I: types as labels
One misspelt property can quietly turn a whole simulation into NaN. TypeScript reads your code before it runs, points at the mistake, and then vanishes without a trace.
- Add type annotations to variables, parameters and return values.
- Describe the shape of an object with an interface.
- Explain what the type checker catches, and when it does its work.
- Explain type erasure, and why data from outside still needs checking at runtime.
A typo that doesn’t crash
Here is one step of a particle simulation. Find the bug:
function step(p, dt) {
p.x = p.x + p.vX * dt;
}
const p = { x: 0, y: 0, vx: 2, vy: 1 };
step(p, 0.5);
console.log(p.x); // NaN
It’s vX with a capital X. The object has no property by that name, so
p.vX is undefined, undefined * 0.5 is NaN (“not a number”), and
NaN plus anything is NaN. There’s no error message. In a real program,
a thousand particles quietly turn into NaN, and you find out an hour
later, somewhere else.
JavaScript can’t complain, because it has no idea what p is supposed
to look like. You know. You just never wrote it down. TypeScript is a way
to write it down, so that a program can check it for you.
Think of labels on kitchen jars: “sugar”, “salt”, “flour”. Before you start cooking, a careful friend reads the whole recipe and says “step 4 puts salt in the sugar jar”. The labels don’t change the food at all. They let someone catch the mistake before the cake is ruined.
Labels on names
let count: number = 0;
let name: string = 'Ada';
function mul(a: number, b: number): number {
return a * b;
}
mul('3', 4);
// error: Argument of type 'string' is not
// assignable to parameter of type 'number'.
A colon and a type after a name is a label. a: number says “a
will always hold a number”. The : number after the parameter list labels
what the function returns. When a call breaks a label, the checker reports
it, with the line and the reason.
You don’t have to label everything. Write let n = 3; and TypeScript
works out the label number by itself. Then n = 'three' is an error
too: “Type ‘string’ is not assignable to type ‘number’.”
TypeScript is JavaScript plus syntax for types, and a program that
checks them. A type annotation, like : number, labels what values a
variable, parameter or return value may hold. Type checking means
reading the whole program, before it runs, and reporting every place
where a value could break a label. Where you leave a label out, the checker
infers one from the value[2].
Interfaces: the shape of an object
Numbers and strings are easy to label. For objects, you describe their shape: which properties they have, and what each one holds.
An interface is a blank form with labelled lines. An object fits the form if it fills in every line with the right kind of value. Ask for a line the form doesn’t have, and the checker says so.
interface Particle {
x: number;
y: number;
vx: number;
vy: number;
}
function step(p: Particle, dt: number) {
p.x = p.x + p.vX * dt;
// error: Property 'vX' does not exist on
// type 'Particle'. Did you mean 'vx'?
}
There’s the typo from the start of the chapter, caught before the program
ever ran. A missing property is caught the same way: writing
const q: Particle = { x: 1, y: 2, vx: 0 }; gets “Property ‘vy’ is
missing”. Lists are labelled with []: number[] is an array of numbers,
and Particle[] is an array of particles.
An interface gives a name to an object shape: the properties it must have, and the type of each. Any object with those properties fits. You don’t need a class or any special way of making it: only the shape matters.
Where do the types go?
Here’s the surprising part. Step through what happens to a TypeScript file on its way to running:
interface Particle {x: number;vx: number;}function step(p: Particle, dt: number) {p.x = p.x + p.vX * dt;}
The TypeScript compiler, tsc, does two jobs. First it checks the
whole file. Then it erases the types and writes out plain JavaScript.
Engines only ever run that JavaScript. One more surprise: by default tsc
writes the JavaScript even when it found errors, and you have to ask
it not to with the --noEmitOnError flag[1].
Type erasure means the types are removed before the code runs. The output is ordinary JavaScript with the labels deleted, so type annotations never change what a program does[1]. All the checking happens before the program starts.
This book’s runnable boxes accept TypeScript, but they only erase the
types (with a tool called sucrase) and run what’s left. They never check
them, so a type error here gets no warning. To see the real checker,
install the typescript package and run tsc on your file. In this
chapter, the tests check what your code does, and the text tells you
what tsc would say.
Try it: the box below runs exactly what erasure leaves.
// The runner erases types and runs
// what is left. It never checks them.
function add(a, b) {
return a + b;
}
console.log(add(2, 3));
console.log(add('2', 3));
// Try: write a: number and b: number,
// then run again. Same output! tsc
// would flag add('2', 3) instead.Labels are promises, not guards
The checker only sees your code. Data arriving from outside the program, like text from a server or a file, was never checked:
const p: Particle = JSON.parse(text);
JSON.parse turns text into an object, and its result has the special
type any, which switches checking off. So the checker takes your word
that this is a Particle. If the text was {"x": "oops"}, you only find
out later, at runtime, as a wrong answer.
At the border, you check by hand. JavaScript’s typeof operator gives the
runtime kind of a value as a string: typeof 3 is 'number' and
typeof 'hi' is 'string'. One famous oddity: typeof null is
'object'. That’s exactly what this chapter’s exercise needs.
Because types are erased, an engine like V8 never sees your interfaces. It
gets plain JavaScript and works out the shapes of objects by itself, while
the program runs[3]. Viscosity is different: it
reads your source, TypeScript included, so an interface like Particle is
a free hint about which fields your objects have. But a type is a promise
about your code, not a guarantee about data (remember JSON.parse). So
the analyses you’ll build later can use types as hints, but they still
have to prove anything they rely on.
Your turn
Types are erased, so data from outside the program is never checked for you. Write isParticle(v) that returns true only when v is an object (not null) whose x, y, vx and vy properties are all numbers, and false otherwise. Extra properties are fine. (The runner erases types, so the tests check behaviour. The walkthrough shows the typed version.)
interface Particle {
x: number;
y: number;
vx: number;
vy: number;
}
function isParticle(v: any): boolean {
// your code here
}
const ok = { x: 0, y: 0, vx: 1, vy: 1 };
console.log(isParticle(ok));tsc rejects this function with “Type 'string' is not assignable to type 'number'” on the return line. Run it first to see what goes wrong at runtime, then fix it so totalX(ms) returns the sum of the x values, and 0 for an empty list.
interface Mover {
x: number;
vx: number;
}
function totalX(ms: Mover[]): number {
let t = '0';
for (const m of ms) {
t = t + m.x;
}
return t;
}
const ms = [
{ x: 1, vx: 0 },
{ x: 2, vx: 0 },
];
console.log(totalX(ms));What you learned
- TypeScript is JavaScript plus type annotations like
x: number, and a checker,tsc. - Type checking happens before the program runs. Where you leave a label out, TypeScript infers one.
- An interface names an object shape. Any object with that shape fits.
- Type erasure:
tscdeletes the types and emits plain JavaScript, so annotations never change behaviour. Engines never see them. - Types describe your code, not outside data. At the border, check with
typeof. This book’s runner erases types but doesn’t check them.
Next: types that say “this or that”, and how the checker follows your
ifs.
Check your understanding
0/6- 1.
With
function mul(a: number, b: number): number, what happens to the linemul('3', 4);when you runtsc? - 2.
What does
tscoutput for the linelet n: number = 3;? - 3.
Which object fits
interface Point { x: number; y: number }? - 4.
You run plain
tsc file.ts(no flags) and it reports a type error. What about the JavaScript file? - 5.
const p: Particle = JSON.parse(text);type-checks fine. Why canp.xstill be a string at runtime? - 6.
Why can’t V8 use your TypeScript interfaces to speed up your program?
References
- [1] The TypeScript team (Microsoft). The Basics (TypeScript Handbook). typescriptlang.org. www.typescriptlang.org/docs/handbook/2/basic-types.html (checked 2026-10-31)
- [2] The TypeScript team (Microsoft). Everyday Types (TypeScript Handbook). typescriptlang.org. www.typescriptlang.org/docs/handbook/2/everyday-types.html (checked 2026-10-31)
- [3] Mathias Bynens, Benedikt Meurer (2018). JavaScript engine fundamentals: Shapes and Inline Caches. mathiasbynens.be. mathiasbynens.be/notes/shapes-ics (checked 2026-10-31)