Runtime Versus Compile Time
Types are erased before your code runs. What that means for validation, reflection and generics, and the checks people expect the type system to perform at runtime.
Types are erased before your code runs. What that means for validation, reflection and generics, and the checks people expect the type system to perform at runtime.
Four things this course has told you do not work, and one explanation covering all of them.
You cannot check value instanceof MyType for a type alias. You
cannot ask what T is inside a generic function. A validated
Photo from response.json() is not validated. And a branded
AccountId is an ordinary string once the program starts.
By the end of this lesson you will know exactly what survives compilation and what does not, so that these stop being four separate surprises and become one fact you can reason from.
type Photo = { id: string; size: number };
interface Uploader {
upload(photo: Photo): Promise<void>;
}
enum Status {
Done = "done",
}
class PhotoStore {
constructor(private readonly limit: number) {}
}
const photo: Photo = { id: "a", size: 1 };Compiled:
var Status;
(function (Status) {
Status["Done"] = "done";
})(Status || (Status = {}));
class PhotoStore {
constructor(limit) {
this.limit = limit;
}
}
const photo = { id: "a", size: 1 };The type and the interface are gone entirely. The private and
readonly modifiers are gone. Photo as an annotation is gone.
Instructions to the checker.
Type aliases, interfaces, type annotations, generic
parameters, access modifiers, satisfies, as and !.
Every one of them is deleted the moment the checker has finished reading it.
Actual JavaScript.
Classes and enums, plus everything you would have written in
plain JavaScript anyway — const, let, function, and the
values themselves.
That is the single fact. Every consequence below follows from it.
Bad — a check against something that no longer exists.
type Photo = { id: string; size: number };
function process(value: unknown) {
if (value instanceof Photo) { // Photo is not a value
...
}
}Good — checking the shape.
function isPhoto(value: unknown): value is Photo {
return (
typeof value === "object" &&
value !== null &&
typeof (value as Record<string, unknown>).id === "string" &&
typeof (value as Record<string, unknown>).size === "number"
);
}The first does not compile — Photo is a type, not a value, so
there is nothing to put on the right of instanceof. That error
is the language telling you the truth: you asked to check
something that will not be there.
The second inspects what actually exists at runtime — an object
with properties — and the value is Photo predicate is how you
report the result back to the checker.
instanceof works for classes, because a class survives:
That is why the errors lesson recommended classes for errors specifically. It is the one place the runtime and the type system agree about identity.
There is no way to ask what T is. The function body is
identical for every call, and the as T is an assertion with
nothing behind it — the generic makes an unchecked cast look
like a feature, as the generics lesson argued.
When behaviour must depend on the type, pass a value:
The schema exists at runtime and does the checking. T is
inferred from it, so the caller writes no type argument and gets
a checked result rather than an asserted one.
That is the general shape: if you need a type at runtime, you need a value that represents it.
Every construct that overrides the checker is a compile-time statement with no runtime effect.
as and ! are you overruling the checker. satisfies
verifies without widening — checked at compile time, emitting
nothing.
So this compiles cleanly and crashes:
Which is the boundary lesson's point, now with the mechanism underneath it: the assertion was a note to a program that had already stopped running.
Several patterns from other languages are unavailable, and knowing why saves you looking.
Overloading by type
Overload signatures describe one implementation, and that implementation has to tell the cases apart itself.
Reflection over types
No listing a type's properties, no constructing one from its name. Tools that appear to do this read your source at build time or use a runtime value instead.
Decorator metadata
Needs an explicit library, because the type information is not there to read.
Telling identical shapes apart
Meters and Feet as bare number aliases are the same
type everywhere — which is exactly what branding was for.
The first one, since the syntax exists and misleads people:
Three signatures, one function body, and the body doing the distinguishing by hand.
Since types cannot be read at runtime, derive the value and the type from one source. Two patterns cover nearly everything.
A constant array, with the union derived:
One declaration gives a runtime list, a compile-time union, and a checked predicate.
A schema, with the type inferred:
The schema is a value that exists at runtime and can check things. The type comes from it, so they cannot disagree.
Both invert the usual direction — the value is the source, and the type is derived — which is the reliable way to have something in both worlds.
Four separate surprises are now one fact: types are instructions to a checker that finishes before your program starts. The practical rule that follows is short — when you need something at runtime, make it a value first and derive the type from it.
Next is TypeScript in CI, the closing lesson of this course. It ties together the checks these lessons have introduced — type checking, linting, tests, builds — into a gate that runs on every change and stays fast enough that nobody works around it.
Before you move on, take any as SomeType in your code and ask
what would happen if the value were the wrong shape. In every
case the answer is the same — nothing, until something much
later fails — and that is the whole of this lesson in one
question.
THE LINE
ERASED type aliases, interfaces, annotations, generic
parameters, access modifiers, as, !, satisfies
SURVIVES classes, enums - the ONLY two that emit code
plus everything you would have written anyway
CONSEQUENCES
no `value instanceof SomeType` a type is not a value
-> a shape check + a `value is T` predicate
instanceof DOES work for classes which is why errors are classes
no way to read T inside a generic
-> pass a value that represents the type (a schema)
as / ! / <T> / satisfies emit NOTHING
-> `await res.json() as Photo` verifies nothing at all
UNAVAILABLE, AND WHY
overloading by type one implementation; check at runtime
reflection over types build-time codegen, or a schema value
decorator metadata needs an explicit library
two identical types are the same type -> brand them
THE TRADE
types never exist at runtime, so they can express far more
than a representable system could - and can do nothing for
you after the build
GETTING BOTH
const STATUSES = [...] as const;
type Status = (typeof STATUSES)[number];
const Schema = z.object({ ... });
type Photo = z.infer<typeof Schema>;
the VALUE is the source; the type is derivedif (error instanceof NotFoundError) { ... }function parse<T>(text: string): T {
return JSON.parse(text) as T; // T is not available here
}function parse<T>(text: string, schema: Schema<T>): T {
return schema.parse(JSON.parse(text));
}const photo = raw as Photo; // no check
const photo = <Photo>raw; // the same, older syntax
const value = maybe!; // no check
const config = { ... } satisfies Config; // no code emittedconst photo = (await response.json()) as Photo;
console.log(photo.size.toFixed(2)); // undefined is not a functionfunction area(shape: Circle): number;
function area(shape: Square): number;
function area(shape: Circle | Square): number {
return "radius" in shape ? ... : ...; // a runtime check
}const STATUSES = ["pending", "done", "failed"] as const;
type Status = (typeof STATUSES)[number];
function isStatus(value: string): value is Status {
return (STATUSES as readonly string[]).includes(value);
}const PhotoSchema = z.object({ id: z.string(), size: z.number() });
type Photo = z.infer<typeof PhotoSchema>;