Type System
TypeScript is not static typing bolted onto the JavaScript runtime — it is a separate layer of checks that is deleted in full before execution. The compiler is two nearly independent halves: the checker reads your annotations, builds a type graph out of them and passes judgement; the emitter simply strips everything type-level and prints ordinary JavaScript. Not one check survives that boundary. Hence the dividing line of the topic: any claim about a type that you make in code (as, !, interface, a generic parameter) is a claim addressed to the checker, and the runtime will never hear it. Anything you need to know at runtime must be expressed as values: a class, a discriminant field, a schema validator.
The other half of the topic is that even inside the checks there are fewer guarantees than you think. TypeScript's type system is structural (a type is a shape, not a name — which is why type UserId = string and type OrderId = string are literally the same type) and deliberately unsound: arrays are covariant, method-shorthand parameters stay bivariant even under strictFunctionTypes, arr[10] is typed T rather than T | undefined by default, and any is not a type at all but a hole that is assignable in both directions and infects everything it touches. None of this is a bug: the TypeScript team wrote "apply a sound or provably correct type system" into the language's explicit non-goals, choosing a balance between correctness and productivity over real-world JavaScript. You can live with that — but only by knowing exactly where the holes run and what closes each one: unknown at the boundary, satisfies instead of an annotation, a brand instead of a bare string, a validator instead of an as. The layer-by-layer breakdown follows.
Topic map
- Type erasure — what
tscphysically deletes from the emit, what survives compilation, and why you can neither branch on a type nor callnew T(). - Type assertions (as) —
ascompiles to nothing and cannot fail, how it differs from a cast, and whyas unknown as Tis an off switch for the checker. - as const — the const assertion turns widening off, gives you
readonlyand tuples, and lets you derive a union straight from a value. - satisfies — the TypeScript 4.9 operator that checks an expression against a type without changing the expression's inferred type.
- Structural typing — a type is identified by shape, not by name; why TypeScript has no nominal typing and where nominality leaks in anyway.
- Branded types — a phantom field in an intersection, the smart constructor as the one legitimate place for
as, and what it costs. - any propagation — why a single
anyat a boundary switches checking off in code that never mentions it, and whyunknownis the right replacement. - Deliberate unsoundness — bivariant methods, covariant arrays, unchecked indexing and the other holes as a conscious trade-off.
Common Mistakes and Traps
| Mistake | Consequence | |
|---|---|---|
Believing as User verifies the data at runtime | as is erased and cannot fail — the crash happens later, in another file | |
Writing await res.json() as User instead of parsing with a schema | You get a type but no check — it is a lie to the compiler, and the first API contract change ships undefined to production | |
Assuming strict protects you from an any that arrived from library types | res.json() is declared as any — an explicit one, and noImplicitAny by definition never fires on it | |
Taking any at a data boundary instead of unknown | any is assignable in both directions and every operation on it yields any again — the hole spreads across the whole inference graph | |
Expecting to check an interface with instanceof or typeof | Neither interface nor type survives to runtime — there is nothing there to check | |
Trying to call new T() or read T.name off a generic parameter | The parameter is erased; pass the constructor as a value: ctor: new () => T | |
| Expecting an overload list to dispatch at runtime | The emit holds one function — the argument triage is written by hand inside the implementation | |
Treating type UserId = string as a distinct type | An alias creates no new type: an OrderId, or any raw string, is accepted silently | |
Believing implements links two types | implements only checks the class's shape; compatibility is still decided structurally | |
Annotating a config (const c: Config = {…}) and expecting the exact keys back | The annotation widens the value to the declared type — the literal keys and values are lost | |
Swapping the annotation for as Config to "keep the type" | as checks nothing: a typo in a key name sails straight through | |
Dating satisfies to the 5.x line | The operator landed in TypeScript 4.9 | |
Expecting as const to freeze anything at runtime | It is a compile-time readonly; the runtime freeze is Object.freeze, and through an alias typed T[] the object is still mutable | |
Treating readonly as protection against a write through another reference | readonly is erased — the same object under a non-readonly type is written to freely | |
Assuming arr[10] is typed `T \ | undefined` | It is not, by default; that is the noUncheckedIndexedAccess flag, which is not part of strict |
Assuming strictFunctionTypes makes method parameters contravariant | Method-shorthand parameters stay bivariant; only function-valued properties become contravariant | |
| Explaining TypeScript's unsoundness as an oversight | It is a documented trade-off between correctness and productivity — defend it as one in the interview |
Interview relevance
This topic is the domain's main filter. The interviewer is probing your execution model, not your syntax: do you understand that there is no channel at all between the checker and the runtime? A single question — "what does as User do at runtime?" — splits people into two groups: those who answer "it casts", and those who answer "nothing; it is erased and cannot fail — which is exactly why validating external data has to be a separate mechanism". The second half of the conversation almost always goes to unsoundness: name a hole, explain why it was left in, and say what you do to close it.
Typical checks:
- What exactly disappears at compile time, and what remains as a value (
class,enum,namespace). - What
asdoes at runtime, and how to tell a legitimate use from a lie to the compiler. - The annotation /
as/satisfiestrio applied to the same config object. - What
as constactually changes, and why a discriminated union breaks without it. - Why
type UserId = stringprotects nothing, and how a brand fixes it. - How
unknowndiffers fromany, and why it is the right type forJSON.parseandcatch. - A concrete unsoundness with its reason: array covariance, bivariant method parameters, unchecked index access.
- How to recover type information at runtime out of erased types (a discriminant, a
class, a schema).
Common wrong answer: "as is a cast, like in Java — if the type does not match you get an error." Almost every habit you will later have to debug grows out of that one belief: await res.json() as User is declared a check, an any is silenced with another as, and a contract mismatch with the backend surfaces three layers away as Cannot read properties of undefined. The second classic failure is defending TypeScript with "if it compiles, it will not crash". If it compiles, the checker found no contradiction among the rules it agreed to check; the array is still covariant, the index was never verified, and nobody looked at the data coming off the wire.