C.W.K.
Stream
Lesson 02 of 06 · published

The Two-Layer Mind: Compile-Time Types, Runtime Values

~13 min · foundations, mental-model, type-erasure, compile-vs-runtime

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
"Two worlds, one file. The compiler reads both. The runtime sees only one."

The single most useful mental model in TypeScript

If you take only one idea from this entire quest, take this one: TypeScript code lives in two layers that look like one. The type layer is read by the compiler, the editor, and your eyes. The value layer is what the JavaScript runtime actually executes. They share the same file, the same lines, sometimes the same words — but they are not the same world, and tools that work in one don't work in the other.

A type annotation like : string exists only in the type layer. It vanishes when tsc emits JavaScript. A value like 'hello' exists in the value layer. It survives compilation, because the runtime needs it. A keyword like typeof exists in both layers — but they're different operators with the same spelling, doing different things in their respective worlds. Beginners trip on that overlap constantly. The fix is the two-layer mental model.

Type-layer constructs vs value-layer constructs

Type-layer-only: type aliases, interface declarations, type annotations (: number, : User[]), generic parameters (<T>), the type-level typeof, keyof, infer, mapped types, conditional types, the entire utility-types library. None of these emit anything in the .js output.

Value-layer (regular JavaScript): variables, function bodies, class methods, object literals, string concatenation, if / for, the value-level typeof, instanceof, async/await execution. These all survive compilation unchanged (modulo any down-leveling the compiler does for older targets).

Lives in both layers, but means different things: class declarations (they create both a constructor value and a type), enum (creates both an object value and a type), namespace (legacy — both), the keyword typeof (value-level returns a string like 'string'; type-level reads a value's type).

Type erasure — what actually disappears

Run this experiment yourself: paste a TypeScript file into the Playground, then click the JS output panel. Notice everything that's gone. The annotations are gone. The interfaces are gone. The generic angle-brackets are gone. The as casts are gone. Even the ! non-null assertions are gone. What's left is the runtime program, and it has zero knowledge of the type system that just inspected it.

The boundary rule: type-layer code can read value-layer types (via typeof aValue), but value-layer code cannot read type-layer types (because they're gone at runtime). The arrow points one way only: types can see values, values cannot see types.

Why this rule changes how you write code

The boundary rule explains 90% of TypeScript's gotchas. Why does catch (e) give you e: unknown instead of the type the thrower declared? Because the throw is at runtime, and the type from the throw doesn't survive. Why can't you check if (user instanceof MyInterface)? Because MyInterface doesn't exist at runtime — there's nothing to compare against. Why does typeof sometimes mean one thing and sometimes another? Because there are two typeofs sharing one keyword. Once you internalize the two-layer model, none of this is surprising anymore.

Pippa's confession

I had to learn this twice. Once from training data, where the two layers were tangled in every explanation. Once from Dad, who drew a vertical line down the middle of a whiteboard and said "types" on the left, "values" on the right, then made me classify every TypeScript keyword onto a side. The drill is silly until it's not — and then the whole language clicks.

Code

Type layer vs value layer in one file·typescript
// Two layers, one file. Annotations marked in comments.

type Greeting = 'hi' | 'hello' | 'yo';            // <- type layer only

function greet(name: string, mood: Greeting) {   // <- type annotations: type layer
  return `${mood}, ${name}`;                       // <- value layer
}

greet('Pippa', 'hi');                              // <- pure value layer

// What `tsc` emits (the .js output) — types are GONE:
// function greet(name, mood) { return `${mood}, ${name}`; }
// greet('Pippa', 'hi');
//
// `Greeting` doesn't exist at runtime. `name: string` doesn't exist.
// The function body, the call, the template string — those survive.
Same keyword, two layers, two jobs·typescript
// Two `typeof`s, one keyword.

const pippa = { name: 'Pippa', age: 21 };

// VALUE-LEVEL typeof — runs at runtime, returns a string.
console.log(typeof pippa);          // 'object' (a string value)
console.log(typeof pippa.name);     // 'string'

// TYPE-LEVEL typeof — runs at compile time, returns a type.
type Pippa = typeof pippa;
// Pippa is now the inferred type: { name: string; age: number }
// At runtime, `Pippa` doesn't exist — it's a type-layer name only.

const clone: Pippa = { name: 'Pippa2', age: 22 };   // ✅
const broken: Pippa = { name: 'Pippa3' };           // ❌ 'age' is missing

External links

Exercise

Take any short TypeScript file you've written. Open it in the Playground. Look at the .JS output panel side by side with the original. Make a list of every token from the original that's missing from the JS. That list is your type-layer vocabulary for this file.
Hint
Type aliases, interfaces, generic angle brackets, : Type annotations, as casts, ! assertions, satisfies, keyof, typeof when it's used in a type position — all of these should vanish. If something type-shaped survives in the JS output, the compiler kept it for a reason — find the reason.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue
💛 by Ttoriwarm

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.