RizTech Academy logo
RizTech Academy
Why TypeScriptLesson 2 of 420 min

How type checking works, and what it cannot do

TypeScript feels like magic at first — it seems to understand your code. It does not; it does something more mechanical and more interesting. Understanding what it actually does, and the one surprising thing it does not do, saves you from a lot of confusion later.

Types are erased — the single most important fact

Here is the thing that surprises everyone, and that half of the "myths" lesson depends on: TypeScript types do not exist when your program runs. TypeScript is a compile-time tool. It checks your types, and then it throws all of them away and emits plain JavaScript.

// what you write
function greet(name: string): string {
  return `Namaste, ${name}`;
}
// what actually runs (the types are gone)
function greet(name) {
  return `Namaste, ${name}`;
}

The : string annotations vanished. The running program is ordinary JavaScript with no idea types ever existed. This is called type erasure, and it has enormous consequences:

  • Types cannot affect runtime behaviour. They do not make code faster, and they cannot check anything while the program runs — a wrong type sneaking in from outside (a bad API response, say) will not be caught by TypeScript at runtime, because there is no TypeScript at runtime.
  • You cannot check a type at runtime the way you check a value. There is no if (x is Item) — the Item type is gone. You check the value's shape with ordinary JavaScript (typeof, in, checking properties), which the narrowing module is all about.

Hold onto this: TypeScript is a checker that runs before your code, not a feature of your running code.

Two phases: check, then compile

So there are two distinct things happening, and it helps to keep them separate:

your .ts files
     │
     ├─ 1. TYPE CHECK   does everything line up? report errors. (this is the value)
     │
     └─ 2. COMPILE      strip the types, emit plain .js. (this is just translation)

The type check is where all the benefit is — it is what catches your bugs. The compile is a mechanical translation to JavaScript (plus, optionally, converting newer syntax to older so it runs in more places). A crucial and slightly alarming detail follows from their separation:

By default, TypeScript will still emit JavaScript even when the type check finds errors. The errors are reported, but the .js is produced anyway — because the errors might be false alarms and you might want to run the code regardless. In a real project you configure the build to fail on type errors (the noEmitOnError option, and your CI), but know that "it compiled" does not by itself mean "it type-checked cleanly".

Structural typing — TypeScript judges by shape, not by name

This is the second big idea, and it is different from Java or C#. TypeScript uses structural typing: two types are compatible if they have the same shape, regardless of their names or where they came from.

type Point = { x: number; y: number };
type Coord = { x: number; y: number };

const p: Point = { x: 1, y: 2 };
const c: Coord = p;          // fine! Point and Coord have the same shape

Point and Coord are different names, never declared as related — but because they have the same shape, a Point is a valid Coord. This is sometimes called "duck typing" — if it walks like a duck and quacks like a duck, it is a duck. A value fits a type if it has at least the properties the type requires:

type Named = { name: string };
const kavita = { name: "Kavita", age: 33 };
const n: Named = kavita;      // fine — kavita has a name (the extra `age` is allowed here)

kavita has more than Named needs, and that is fine — it satisfies the shape Named demands. Structural typing is why TypeScript feels lightweight: you rarely have to formally declare "this class implements that interface"; if the shape matches, it fits. (There is one wrinkle — a stricter check for object literals passed directly — which the objects module covers.)

Inference — TypeScript figures out types you did not write

The third thing that makes TypeScript pleasant: it infers types you do not annotate.

let city = "Pune";           // TypeScript infers: string
const nums = [1, 2, 3];      // infers: number[]
const doubled = nums.map(n => n * 2);   // infers n is number, result is number[]

You did not write a single type, yet TypeScript knows city is a string, nums is number[], and in nums.map(n => ...) that n is a number. It works out types from the values and from how they flow through your code. This is why good TypeScript is often barely more verbose than JavaScript — you annotate the boundaries (function parameters, external data) and let inference handle the inside. The inference lesson goes deep on this; for now, know that not writing a type does not mean there is no type.

What the checker cannot do

Being honest about the limits prevents false confidence:

  • It cannot check data from outside your program. An API response, a JSON.parse, user input — TypeScript will happily believe your annotation, but it does not verify the data matches at runtime (types are erased). Typing external data is a trust-but-verify problem the real-projects module addresses.
  • It cannot catch logic bugs. return a - b when you meant a + b type-checks perfectly — both are numbers. TypeScript checks types, not correctness. You still need tests.
  • It is not sound in every corner. For pragmatism, TypeScript allows a few unsafe things (like any, and some array-access assumptions) that can let a type error slip through. It aims to catch the overwhelming majority of real bugs, not to be a mathematical proof.

None of these are failures — they are the boundary of what a type checker can do. Knowing the boundary tells you where you still need care: at the edges (external data) and for correctness (tests).

Check your work

Type erasure. TypeScript checks types then removes them; the running program is plain JavaScript with no types.

Two consequences of erasure. Types cannot affect runtime behaviour or performance, and you cannot check a type at runtime — only a value's shape.

The two phases. Type check (where the value is) and compile (mechanical translation to JS).

Why "it compiled" is not "it type-checked". By default TypeScript emits JS even with type errors; you configure the build/CI to fail on them.

Structural typing. Two types are compatible if their shapes match, regardless of names — a value fits if it has at least the required properties.

Inference. TypeScript works out types you did not annotate, from values and how they flow, so typed code is barely more verbose than untyped.

Three things the checker cannot do. Verify external data at runtime, catch logic bugs, or be perfectly sound.

Practice

  1. Write a typed function, compile it (tsc), and look at the emitted .js. Confirm the types are gone.
  2. Introduce a type error and compile. Note that the .js is still produced (unless you set noEmitOnError).
  3. Declare two differently-named types with the same shape and assign one to the other. Confirm it is allowed (structural typing).
  4. Assign an object with extra properties (via a variable) to a type that requires fewer. Confirm it fits.
  5. Declare a few values with no annotations and hover in your editor (or use the Playground) to see the inferred types.
  6. Write a logic bug (a - b for a + b) that type-checks. Note that TypeScript cannot catch it — only a test can.
  7. Explain, in one sentence, why TypeScript cannot guarantee an API response matches the type you gave it.

Official documentation

Next: what TypeScript is not — the myths worth clearing up early.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship