Type System
What TypeScript's types actually are — compile-time erasure, assertions versus satisfies, as const, branded types that simulate nominal typing, how any poisons inference, and where the type system is deliberately unsound.
13 questions
JuniorTheoryVery commonWhich TypeScript constructs survive into the emitted JavaScript, and which vanish?
Which TypeScript constructs survive into the emitted JavaScript, and which vanish?
Everything type-level is erased: annotations, interface, type aliases, generics and as all disappear, and the output is plain JavaScript with unchanged semantics. Only value-level constructs emit code — class, enum, namespace.
Common mistakes
- ✗Expecting a wrong API payload to be rejected at runtime by its declared type
- ✗Trying to reach a generic parameter
Tfrom inside the function body at runtime - ✗Believing
strictadds runtime checks rather than compile-time ones
Follow-up questions
- →Why does
enumemit code while atypealias does not? - →How do you check the shape of an API response given that types are erased?
JuniorTheoryVery commonWhat does an as assertion do at runtime, and how can it fail?
What does an as assertion do at runtime, and how can it fail?
Nothing, and it cannot fail. as is erased: it only instructs the compiler to treat the expression as another type, performing no conversion and no check. So await res.json() as User is a claim about the payload, not a verification of it.
Common mistakes
- ✗Treating
asas a cast that converts or validates the value - ✗Trusting
res.json() as Useras though the payload had been checked - ✗Reaching for
asto silence an error instead of narrowing with a real guard
Follow-up questions
- →Why does the compiler reject
asbetween two completely unrelated types? - →What would you write instead of
asto actually verify a payload?
JuniorTheoryCommonWhat stops being checked once a value has the type any?
What stops being checked once a value has the type any?
Everything about that value. any is assignable in both directions, so you may read any property, call it, index it, and pass it anywhere without a complaint. Each of those operations yields any in turn, so it spreads downstream.
Common mistakes
- ✗Assuming a property read off an
anygivesunknownrather than anotherany - ✗Expecting
noImplicitAnyto catch ananyyou wrote explicitly - ✗Believing a typed parameter re-checks an
anyargument passed into it
Follow-up questions
- →Why does
unknowncontain the same values asanyyet stay safe? - →Which compiler flag would surface an
anythat leaked in from a library?
MiddleTheoryCommonWhere is any genuinely the right type, and where is it a smell?
Where is any genuinely the right type, and where is it a smell?
It is defensible in a constraint position a caller never sees — T extends (...args: any[]) => any — and in internal plumbing where the real type is re-established on the way out. At a data boundary it is a smell: unknown forces a guard.
Common mistakes
- ✗Typing an external payload
anywhenunknownaccepts it and keeps checking on - ✗Assuming
anyandunknownbehave the same because they accept the same values - ✗Spreading
anythrough internal helpers where the real type was available
Follow-up questions
- →Why is
any[]acceptable inside a generic constraint but not in a public signature? - →What does a caught
errorhave as its type, and what should you do with it?
MiddleTheoryCommonWhat does as const change about an object or array literal?
What does as const change about an object or array literal?
It switches off widening: every property becomes readonly, each literal keeps its literal type instead of widening to string or number, and an array becomes a readonly tuple. That is what lets you derive a union with typeof COLORS[number].
Common mistakes
- ✗Expecting
as constto freeze the object at runtime - ✗Confusing it with a
constdeclaration, which only fixes the binding - ✗Forgetting that it makes an array a readonly tuple, not a mutable array
Follow-up questions
- →Why does
typeof COLORS[number]only give a useful union afteras const? - →What breaks when you pass an
as constarray to a parameter typedstring[]?
JuniorTheoryOccasionalWhat problem does a branded (opaque) type solve?
What problem does a branded (opaque) type solve?
TypeScript compares types by structure, so type UserId = string is simply string: an OrderId, or any raw string, is accepted where a UserId belongs. A brand intersects the base type with a phantom marker that no plain string carries.
Common mistakes
- ✗Believing a plain
typealias already creates a distinct type from its base - ✗Expecting the brand to exist as a real property on the value at runtime
- ✗Handing out an
ascast to the branded type instead of routing through one factory
Follow-up questions
- →Why does the brand cost nothing in the emitted JavaScript?
- →What stops a caller from bypassing the factory with an
asassertion?
MiddleCodeOccasionalImplement a UserId that a plain string cannot satisfy
Implement a UserId that a plain string cannot satisfy
Intersect the base type with a phantom field keyed by a unique symbol, so no plain string is assignable to it. Expose one factory that validates the raw value and asserts it into the branded type. The brand is type-only, so the emitted JavaScript is unchanged.
Common mistakes
- ✗Believing a bare
typealias orstring & {}already excludes a plain string - ✗Exporting the brand so callers can hand-craft a
UserIdwith anas - ✗Reaching for a wrapper class and paying an allocation on every id
Follow-up questions
- →Why keep the branding symbol unexported from the module?
- →How would you brand a
numberid and anOrderIdwithout them colliding?
MiddleTheoryOccasionalTypeScript is deliberately unsound — name a concrete example and the reason
TypeScript is deliberately unsound — name a concrete example and the reason
Arrays are covariant, so a Dog[] is accepted where an Animal[] is wanted and a Cat can then be pushed into it. Method parameters stay bivariant even under strictFunctionTypes, and as and any switch checking off outright.
Common mistakes
- ✗Believing
strictmakes the type system sound - ✗Naming only
anyand missing the variance holes in arrays and methods - ✗Calling the unsoundness a bug rather than a deliberate ergonomics trade-off
Follow-up questions
- →Why are method parameters bivariant while a function property is contravariant?
- →What would break in everyday code if arrays were made invariant?
MiddleTheoryOccasionalWhat does satisfies give you that an annotation and an as do not?
What does satisfies give you that an annotation and an as do not?
satisfies, added in TypeScript 4.9, checks the expression against a type while keeping the type inferred from the literal. An annotation checks and then widens, losing the exact keys and values; as keeps the type but checks nothing.
Common mistakes
- ✗Believing
satisfiesperforms a runtime check on the value - ✗Expecting an annotation to preserve the literal keys of an object
- ✗Reaching for
aswhen you wanted both a check and the inferred type
Follow-up questions
- →What does
keyof typeof cfggive you aftersatisfiesthat an annotation loses? - →Why does
satisfiesstill leave an excess property an error?
SeniorTheoryOccasionalWhy can overload signatures not dispatch at runtime the way Java overloads do?
Why can overload signatures not dispatch at runtime the way Java overloads do?
Because overloads are types, and types are erased: the emitted JavaScript holds one function. The overload list only describes call signatures to the checker; the single implementation must accept every case and dispatch by hand inside that body.
Common mistakes
- ✗Expecting the compiler to emit a dispatcher from the overload list
- ✗Writing overload bodies rather than one implementation that handles every case
- ✗Assuming the implementation signature is visible to callers
Follow-up questions
- →Why is the implementation signature not callable from outside?
- →When is a single union-typed signature clearer than an overload set?
SeniorDebuggingOccasionalA typo on a parsed payload compiles cleanly under strict — diagnose why
A typo on a parsed payload compiles cleanly under strict — diagnose why
Follow the any upstream. res.json() is declared to return any, so data and everything derived from it are any too, and the checker stops flagging typos and wrong arguments. noImplicitAny never fires — this any is explicit.
Common mistakes
- ✗Expecting
noImplicitAnyto catch ananythat a library declared explicitly - ✗Annotating
dataasOrderand mistaking that annotation for a runtime check - ✗Blaming
strictfor being off rather than tracing where theanyentered
Follow-up questions
- →Which lint rule would have surfaced this
anyat review time? - →Why does typing
dataasunknownforce the bug to the surface immediately?
SeniorTheoryRareTypes are erased, so how do you obtain type information at runtime?
Types are erased, so how do you obtain type information at runtime?
You do not get it from the type system; you re-create it as values. Keep a discriminant field on the data, use a class whose constructor survives for instanceof, or run a schema validator that checks at runtime and infers the static type.
Common mistakes
- ✗Expecting a generic parameter
Tto be inspectable from inside the function body - ✗Believing
emitDecoratorMetadatarecords full generic or interface shapes - ✗Treating an annotation at a boundary as a runtime check on the payload
Follow-up questions
- →Why does a
classgive youinstanceofwhile aninterfacedoes not? - →How does a schema validator manage to be both the check and the source of the type?
SeniorDesignRareAfter discovering satisfies, a teammate opens a pull request replacing every type annotation in the codebase with it — on exported function return types, on public interfaces implemented by classes, and on the config objects where it was first introduced. The argument is that satisfies always checks at least as much as an annotation while never losing information, so an annotation is now strictly redundant. Explain where that reasoning holds and where it breaks, what an annotation buys you on a public API surface that the inferred type does not, and what policy you would write down for the team covering annotations, satisfies, and as.
After discovering satisfies, a teammate opens a pull request replacing every type annotation in the codebase with it — on exported function return types, on public interfaces implemented by classes, and on the config objects where it was first introduced. The argument is that satisfies always checks at least as much as an annotation while never losing information, so an annotation is now strictly redundant. Explain where that reasoning holds and where it breaks, what an annotation buys you on a public API surface that the inferred type does not, and what policy you would write down for the team covering annotations, satisfies, and as.
Use satisfies where you want both the check and the precise inferred type — config maps, route tables, as const data you index into. Keep a plain annotation on a public API surface, where the declared type is the contract and widening is the point, not a loss.
Common mistakes
- ✗Treating an inferred type as a safe substitute for a declared public contract
- ✗Assuming
satisfiesis a weaker check than an annotation rather than a different one - ✗Letting
asstand in for either, since it checks nothing at all
Follow-up questions
- →How does an inferred return type on an exported function widen the blast radius of a refactor?
- →Why does
satisfiesstill leave excess-property checking in force?