Is a TypeScript `abstract` class enforced at runtime? Show what `abstract class Repo { abstract find(id: string): void }` compiles to and what that means for callers.
answer
- the type layer stops at build time
- keyword stripped, class still emitted
- member had no body to emit
- unlike interface, the binding survives
- new.target if you want it enforced
basics
~20 sNo. abstract is a compile-time modifier only: TypeScript emits an ordinary JavaScript class with the keyword stripped and the abstract members gone entirely, so nothing at runtime prevents an untyped caller from constructing it or from missing a member.
solid answer
~50 sIt is compile-time only. `abstract class Repo { abstract find(id: string): void }` emits `class Repo {}` — the keyword is stripped and the abstract member emits nothing at all, because it was only ever a type-level declaration with no body. So the class binding does exist as a runtime value, unlike an `interface`, which emits nothing whatsoever; but the non-instantiability is a promise the checker keeps, not a guard in the code. `new (Repo as any)()` succeeds and hands you an object whose `find` is `undefined`, and the same is true for plain JavaScript or JSON-driven code calling in from outside the type checker. If you genuinely need the runtime guarantee — say, in a published library consumed from JavaScript — you add it yourself in the constructor, typically with a `new.target` check, and that is ordinary JavaScript rather than anything the type layer gives you.
code
typescript · 16 linesabstract class Repo {
abstract find(id: string): void;
abstract tableName: string;
describe(): string {
return `repo for ${this.tableName}`;
}
}
class UserRepo extends Repo {
tableName = 'users';
find(id: string): void {
console.log(`SELECT * FROM ${this.tableName} WHERE id = ${id}`);
}
}
console.log(new UserRepo().describe());go deeper
Remember that abstract is checked by the compiler and vanishes from the output, so the protection only applies to type-checked code.
Be able to write the emitted JavaScript from memory: keyword stripped, abstract members absent, implemented methods intact — and explain why the class binding still exists while an interface's does not.
Diagnose the real-world symptom: a TypeError about an undefined method far from the construction site, traced back to untyped or asserted code that bypassed the check, and decide whether a new.target guard is warranted at that boundary.
Set the policy on where erased guarantees are acceptable: which package boundaries face untyped consumers and therefore need real runtime validation, versus internal code where compile-time checking is the whole contract.
## Types are erased, and `abstract` is a type TypeScript's whole design is a compile-time type layer over JavaScript: it checks, then emits JavaScript with the type information removed. `abstract` sits squarely in that layer. It changes what the checker allows and changes nothing about the program that actually runs. ## What the emit looks like Input: ```ts abstract class Repo { abstract find(id: string): void; abstract tableName: string; describe(): string { return `repo for ${this.tableName}`; } } ``` Output (modern target): ```js class Repo { describe() { return `repo for ${this.tableName}`; } } ``` Three things to read off that: 1. The `abstract` keyword on the class is gone. Nothing replaces it — no thrown error, no marker property, no check. 2. The abstract **method** emits nothing. It had no body, so there is nothing to emit; `Repo.prototype.find` does not exist. 3. The abstract **property** emits nothing either. It is a type-level declaration, not a field initializer, so no assignment and no property definition appears in the constructor. Yet `describe`, an ordinary implemented method, emits normally and reads `this.tableName` as if it were there — because at check time it is. ## The class is a value; an interface is not This is the distinction worth holding on to. An `interface` emits *nothing at all* — the declaration disappears completely and the name exists only in the type space. An abstract class emits a **real class binding**: it occupies a slot in the module's exports, participates in the prototype chain of its subclasses, and can be referenced from JavaScript at runtime. What it loses relative to a concrete class is only a compile-time permission. ```ts abstract class Base {} class Impl extends Base {} const x: Base = new Impl(); console.log(Object.getPrototypeOf(Impl) === Base); // true at runtime ``` ## Where the promise leaks Because the guarantee lives in the checker, it holds exactly as far as the checker's reach: ```ts const leaked = new (Repo as any)(); leaked.find('42'); // TypeError: leaked.find is not a function ``` The assertion silences the checker — an assertion is a claim, not a conversion, and it performs no check of its own — and the construction then succeeds because the emitted constructor has no objection. You get a `Repo` instance with no `find` and no `tableName`. The same door is open to any caller outside type checking: a plain JavaScript consumer of your compiled package, a `require` from an untyped script, a reflective factory that looks classes up by name in a registry. This is worth stating plainly in an interview because it is the general shape of every TypeScript guarantee, not a quirk of abstract classes. The type layer buys you errors at build time and nothing at all at 3 a.m. in production. ## Adding the runtime check when you need one If a class is part of a package boundary where untyped callers are realistic, you enforce it yourself in ordinary JavaScript: ```ts abstract class Repo { constructor() { if (new.target === Repo) { throw new TypeError('Repo is abstract and cannot be instantiated directly'); } } abstract find(id: string): void; } ``` `new.target` is the constructor actually invoked with `new`, so it equals `Repo` only for a direct `new Repo()` and equals the subclass for `new SomeImpl()`. That check costs one comparison per construction and is the honest way to get the behaviour people wrongly assume `abstract` already provides. Whether it is worth writing depends entirely on whether untyped callers exist: inside an application compiled as one unit, they generally do not, and the compile-time check is the whole story. ## The practical consequences - **Do not rely on abstractness for security or validation.** A malformed object reaching your abstract type's methods is a data-validation problem, solved by parsing at the boundary, not by the class modifier. - **Do not expect a helpful runtime error** when something goes wrong. You will not see "cannot instantiate abstract class"; you will see `undefined is not a function` at some later call site, which is a much worse diagnostic. Knowing the emit explains that message instantly. - **Do expect the class to exist as a value.** Extending it, referring to it from a registry, or using it as an operand where a class object is needed all work, because there is a real constructor function there. - **A missing abstract member is not a missing method at runtime** — it is a member that was never emitted, which is why the failure surfaces as `undefined` rather than as an inheritance problem.
- If the abstract members emit nothing, what actually goes wrong at runtime when someone bypasses the check and instantiates the abstract class?You get an object that is missing those members entirely. Calling one fails with a `TypeError` saying the property is not a function, and reading an abstract property yields `undefined`. The failure surfaces wherever the member is used, not at construction, which is why the diagnostic is usually far from the real mistake.
- How would you enforce non-instantiability at runtime for a class shipped to JavaScript consumers?Add a guard in the constructor: `if (new.target === Base) throw new TypeError(...)`. `new.target` is the constructor actually invoked, so it equals the base only on a direct `new Base()` and equals the subclass otherwise. It is plain JavaScript — the type layer contributes nothing to it.
- Both an interface and an abstract class can be used as a type annotation. What is the runtime difference between the two declarations?The interface emits nothing at all — the name exists only in the type space and cannot be referenced from JavaScript. The abstract class emits a real class binding that can be exported, extended, and referenced as a value at runtime; it merely refuses `new` at compile time.
saying these in an interview costs you the question
- Says instantiating an abstract class throws at runtime
- Thinks abstract classes disappear from the emit like interfaces
- Expects abstract members to exist as stub methods on the prototype
- Treats the compile-time check as a security or validation boundary
- Believes an `as any` cast changes what the constructor does