In TypeScript, given `type UserId = string & { readonly __brand: 'UserId' }`, what JavaScript does `const id = 'u_1' as UserId` compile to, and what does reading `id.__brand` give you at runtime?
answer
- types do not survive compilation
- as is an assertion, not a conversion
- the marker never becomes a real property
- emitted line is the bare string literal
- reading a missing property yields undefined
basics
~20 sIt compiles to const id = 'u_1';. The type and the assertion are erased, so the value is an ordinary string primitive and id.__brand is undefined at runtime — the brand exists only during type checking.
solid answer
~40 sThe emitted JavaScript is just `const id = 'u_1';`. TypeScript erases the whole type layer, and `as` is an assertion rather than a conversion, so it emits nothing at all — no property is attached, no object is allocated, no check is performed. At runtime `typeof id` is `"string"` and `id.__brand` evaluates to `undefined`, because the property never existed; the compiler happily *types* that expression as `'UserId'` while the runtime has nothing to hand back. That is what "phantom brand" means: the marker property is a compile-time fiction whose only job is to make the branded type structurally different from a plain string. The practical consequence is that branding is free — zero allocation, zero indirection — and equally that nothing at runtime can tell a branded value from any other string.
code
typescript · 6 linestype UserId = string & { readonly __brand: 'UserId' };
const id = 'u_1' as UserId;
console.log(typeof id); // "string"
console.log(id.__brand); // typed as 'UserId', prints undefinedgo deeper
Be able to state that types and assertions are erased, so the emitted line is just the string literal and the marker property is never created. Say typeof gives "string".
Explain why the compiler still types id.__brand as the literal payload while the runtime returns undefined — the checker reports what the assertion told it, not what exists.
Draw the operational conclusion: because nothing survives compilation, a brand crossing a process or network boundary carries no protection, and re-branding on the far side must re-validate.
Frame erasure as the boundary that decides which invariants may live in types at all, and set the team's rule for what must instead be enforced by runtime validation at trust boundaries.
## What actually gets emitted TypeScript's compilation model is erasure: it type-checks, then emits JavaScript with every type construct removed. A branded alias is a type declaration, so it emits nothing; an `as` expression is an assertion about a value's type, so it also emits nothing. ```ts // input type UserId = string & { readonly __brand: 'UserId' }; const id = 'u_1' as UserId; console.log(typeof id); ``` ```js // emitted JavaScript "use strict"; const id = 'u_1'; console.log(typeof id); ``` The alias line disappears entirely. The `as UserId` disappears entirely. What is left is a plain string primitive. `typeof id` prints `"string"`. ## Reading the brand property Because `id` is *typed* as an intersection containing `{ readonly __brand: 'UserId' }`, the compiler will let you write `id.__brand` and will tell you its type is the literal `'UserId'`. Run it and you get `undefined`: reading a missing property off a string primitive does not throw, it yields `undefined`. This is the sharpest demonstration in the whole pattern of the gap between the type layer and the runtime — the compiler is not lying about a fact, it is reporting what it was *told* by the assertion, and no one ever made that assertion true. ```ts const id = 'u_1' as UserId; console.log(id.__brand); // type says 'UserId', runtime prints undefined ``` ## Why the brand is deliberately phantom A brand whose property genuinely existed would cost something: an object wrapper, or a property attached to a boxed value. The whole appeal of the intersection brand is that it costs nothing. The marker exists purely to make `UserId` structurally distinct from `string` **during checking**, and structural distinctness is decided at compile time. Once checking has succeeded, the marker has done its job and can vanish. This also explains why the property is conventionally declared `readonly` and given a name nobody would use by accident, such as `__brand`. Both are conventions aimed at the reader and at avoiding a collision with a real data property — neither has any runtime effect. ## What this means in practice Four consequences follow directly. 1. **No runtime cost.** Branding a hot-path id type does not allocate, does not add a property lookup, and does not change serialization. `JSON.stringify(id)` produces `"u_1"`, exactly as for any string. 2. **No runtime protection.** Nothing downstream can inspect a value and discover whether it was branded. If a wrong id crosses a network boundary, the receiving process gets a string like any other. 3. **Structural checks on the brand are meaningless.** Code such as `if ('__brand' in id)` is always false. Do not write runtime checks against a phantom marker. 4. **The assertion is the only gate.** Since the compiler performs no check when you write `as UserId`, the correctness of every branded value in the program depends entirely on the code that performed the assertion. ## Where erasure is *not* the rule It is worth knowing the boundary. Most of TypeScript emits nothing — interfaces, type aliases, generics, assertions, type annotations. The exceptions that do emit runtime code are `enum` (which builds a real object unless declared `const`) and legacy decorators enabled with `experimentalDecorators`, along with standard decorators, which are a JavaScript-level feature. A branded type belongs firmly in the erased group: there is no configuration under which the marker property becomes real. ## Answering this in an interview The expected answer is short and exact: the emitted line is `const id = 'u_1';`, `typeof id` is `"string"`, and `id.__brand` is `undefined`. Adding the reason — assertions are not conversions and types do not survive compilation — turns a recall answer into an understanding one.
- If the brand does not exist at runtime, what stops a wrong value from being branded in the first place?Nothing automatic. `as` performs no check, so the guarantee is entirely a code-organisation one: the assertion is confined to a factory that validates first, and no other site is allowed to write it. A lint rule banning assertions to branded types outside their owning module is the usual enforcement. Treat a brand as a claim about where a value came from, not as evidence about the value itself.
- Are there TypeScript constructs that do emit runtime code, unlike a branded type?Yes — `enum` emits a real object at runtime unless it is declared `const`, and decorators (both the standard form and the legacy `experimentalDecorators` form) emit calls, because they are a runtime feature rather than a type-layer one. Everything in the type layer proper — interfaces, aliases, generics, annotations, assertions — is erased, and a branded type is squarely in that group.
saying these in an interview costs you the question
- Thinks the __brand property is attached to the value at emit
- Says `as` performs a conversion or a runtime check
- Expects reading id.__brand to throw rather than return undefined
- Writes a runtime check like '__brand' in id
- Believes a compiler flag can make the brand real at runtime