TypeScript's types are erased before the code runs, so how can a validation or ORM library that uses decorators know at runtime that a class property is supposed to hold a string?
answer
- types vanish, code does not
- who is still around at runtime?
- decorators run when the class is defined
- a side table keyed by class and property
- design:type only with the two flags
basics
~20 sThe annotation itself is gone after compilation. Decorators are ordinary functions that run when the class is defined, so the library records what it needs in a runtime data store at that moment — either from an explicit argument or from compiler-emitted metadata.
solid answer
~40 sA type annotation like `title: string` produces no runtime artefact at all — the emitted JavaScript has only the field. So a library cannot inspect the type; it has to be *told*. Decorators are the telling mechanism: a decorator is a normal function that executes once, at class-definition time, and it stores whatever it was given in a side table keyed by the class and property. `@IsString()` works because the string-ness is in the decorator call, not in the annotation. TypeScript can also help: with `experimentalDecorators` plus `emitDecoratorMetadata`, the compiler emits a `design:type` entry next to each decorated member, which the `reflect-metadata` library exposes. Either way the knowledge lives in runtime data the library wrote down, never in the type.
go deeper
Be ready to say plainly that annotations are erased and that a decorator is real code that runs when the class is defined. Naming one library use case — validation or column mapping — is enough here.
Explain the two routes a fact reaches the runtime store: passed as a decorator argument, or emitted by the compiler as design:type when experimentalDecorators and emitDecoratorMetadata are both on and reflect-metadata is loaded.
Show that you treat decorator registration as module-load side effects: discovery depends on imports, ordering, and bundler behaviour, and a silently empty metadata store is a far more common production failure than a wrong rule.
Own the consistency question — a hand-written decorator argument can drift from the annotation it mirrors. Argue when to derive runtime shape from a schema-first source instead, so one declaration produces both the type and the validator.
## The starting fact: nothing survives TypeScript compiles to JavaScript by deleting the type layer. An interface emits no code, a type alias emits no code, and a property annotation is stripped from the field declaration. This class: ```typescript class Post { title: string = ""; } ``` becomes, in the emitted JavaScript, a class with a `title` field initialised to `""` and nothing else. There is no object anywhere in the running program that says "`title` is a string". So any library that wants to validate, serialise, or map a property to a database column at runtime cannot read the type — the information does not exist by then. ## Decorators are runtime code A decorator is the exception to "TypeScript adds no runtime cost". `@Column()` is not an annotation the checker consumes and discards; it compiles to a real function call that runs once, when the class definition is evaluated. That gives a library a hook at exactly the moment the class comes into existence. What the library does with that hook is bookkeeping. It writes an entry into a store — a `Map`, an object hung off the constructor, or a metadata table — that says "class `Post`, property `title`, rule: must be a string". Later, when `validate(post)` or `repository.save(post)` runs, the library looks the class up in that store and finds the rules. The type system was never involved at runtime; a data structure was built during module evaluation. ## Two ways the fact gets into the store **Explicitly, as a decorator argument.** This is the common and portable route. `@IsString()`, `@Column({ type: "varchar" })`, `@MaxLength(80)` all carry the fact in the call itself. The decorator does not need to know anything about TypeScript; it would work identically in plain JavaScript. The cost is duplication: you write `title: string` for the checker and `@IsString()` for the runtime, and nothing forces the two to agree. **Implicitly, via compiler-emitted metadata.** TypeScript can close part of that gap. With both `experimentalDecorators` and `emitDecoratorMetadata` enabled, the compiler emits, for each *decorated* declaration, extra entries describing the declared types: `design:type` for a property or method, `design:paramtypes` for a constructor or method parameter list, and `design:returntype` for a method's return. These are written through `Reflect.metadata(...)`, which only exists if the `reflect-metadata` polyfill has been imported, so applications using this route import it once at startup. A library then reads it back: ```typescript import "reflect-metadata"; const t = Reflect.getMetadata("design:type", Post.prototype, "title"); console.log(t === String); // true ``` Note what had to happen for that to work: the compiler *serialised* the type into a runtime value. `string` became the `String` constructor. That is a lossy translation, and only a handful of types have a runtime value to point at. ## Why this matters even at a junior level The practical consequence is that a decorator-driven library can only enforce what someone recorded. If you change `title: string` to `title: number` and forget to change `@IsString()`, the checker and the validator now disagree and nothing warns you — unless the project is on the `emitDecoratorMetadata` route, where the emitted `design:type` follows the annotation automatically. It also explains a family of confusing bugs. A validation decorator that "stops working" is usually a decorator that never ran (the module was never imported, so the class body never evaluated), or a metadata read against the wrong target object, or a missing `reflect-metadata` import. None of these are type errors, because none of this is type-checked; it is ordinary runtime data flow that happens to be written in an unusual place. ## The bridge, stated plainly Types are a compile-time argument between you and the checker. Decorators are runtime code. Metadata is the bridge: a decorator running at class-definition time copies a fact into a runtime store so that code executing much later can act on it. Everything a decorator-based framework does — dependency injection, column mapping, request routing, validation — is that same pattern with a different table.
- If the fact has to be written down anyway, why not just call a registration function after the class instead of using a decorator?You can, and some libraries do. The decorator's advantage is locality — the rule sits on the member it describes, so it cannot drift to the wrong property name during a rename. The cost is that it needs decorator support in your build, and the registration is now a hidden side effect of importing the module.
- What happens if the module containing a decorated class is never imported?Nothing is registered. The class body never evaluates, so the decorators never run and the library's store has no entry for it. This is why frameworks that discover entities or handlers by decorator require you to import or glob-load every such module, and why a "missing entity" error is usually an import problem, not a configuration one.
- Does a decorator change the declared type of the property it is attached to?With `experimentalDecorators`, no — the decorator's return value is applied at runtime but the checker keeps the declared type exactly as written. So anything a decorator adds or transforms at runtime is invisible to the type system, and callers see the original shape.
saying these in an interview costs you the question
- Thinks the checker validates values at runtime
- Says the type annotation is readable via reflection
- Assumes decorators run on every instantiation
- Believes design:type appears without any compiler flag
- Confuses the decorator running with the decorated code running