"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.
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.