skip to content

Classes & Decorators

The type-layer features that only exist on classes: visibility modifiers, readonly and parameter properties, abstract members, implements clauses, this typing, and decorators. Interviewers lean on this area because most real TypeScript codebases — Angular, NestJS, TypeORM — are class-and-decorator shaped, and the compile-time-only nature of these features trips people up.

part ofTypeScriptoverview, primer and where to startread it →
on this pageshow

explore

questions

page 1 of 2

In TypeScript, what does the `abstract` modifier mean when it is written on a class and when it is written on one of that class's members, and which errors does the compiler raise around it?

level: juniorimportance: must knowfreq 72%

answer

  1. incomplete on purpose
  2. class: base only, never new
  3. member: signature, no body
  4. concrete subclass must fill every hole
  5. not with static, not with private

basics

~20 s

abstract on a class blocks creating instances of that class directly. abstract on a member declares a signature with no body that every concrete subclass must supply. TypeScript rejects instantiating the class and rejects a non-abstract subclass that leaves a member unimplemented.

solid answer

~50 s

`abstract` marks a declaration as deliberately incomplete. On a class it means "base only": writing `new Shape()` fails with *Cannot create an instance of an abstract class*, though the class can still hold fields, a constructor, and fully implemented methods that subclasses inherit. On a member — a method, a property, or an accessor — it means the member is part of the instance type but this class supplies no code: `abstract area(): number;` has a signature and no body. Any non-abstract subclass then has to implement every inherited abstract member, or you get *Non-abstract class 'Circle' does not implement inherited abstract member 'area' from class 'Shape'*. An abstract member may only appear inside an abstract class, may not have a body, and may not be combined with `static` or `private` — `protected abstract` is fine. All of this is checked at compile time; the modifier is erased from the emitted JavaScript.

code

typescript · 22 lines
typescript
abstract class Shape {
  readonly name: string;
  constructor(name: string) {
    this.name = name;
  }
  describe(): string {
    return `${this.name} has area ${this.area()}`;
  }
  abstract area(): number;
}

class Square extends Shape {
  constructor(private side: number) {
    super('square');
  }
  area(): number {
    return this.side * this.side;
  }
}

const shapes: Shape[] = [new Square(3)];
console.log(shapes[0].describe());

go deeper

for a junior

Be able to write an abstract class with one abstract method and a subclass that implements it, and to say plainly that you cannot call new on the abstract class itself.

for a middle

Explain the exact compiler errors and their triggers, that abstract members carry no body, that only an abstract subclass may leave holes open, and why static abstract and private abstract are rejected.

for a senior

Show the template-method shape in real code: shared algorithm in the base, mandatory hooks as abstract members, and be ready to say what the compile-time guarantee does and does not cover at the boundaries of your codebase.

for a principal

Own the call on whether a mandatory-hook base class is the right extension point for a library at all, given that it consumes the single extends slot and couples every implementer to your base's evolution.

## The one-line model `abstract` marks a declaration as **deliberately incomplete**. On a class it says: this exists to be extended, never instantiated on its own. On a member it says: this member belongs to the instance type, but this class does not provide the code — a concrete subclass must. Both statements are enforced by the type checker only; the modifier itself does not survive into the emitted JavaScript. ## `abstract` on the class ```ts abstract class Shape { readonly name: string; constructor(name: string) { this.name = name; } describe(): string { return `${this.name} has area ${this.area()}`; } abstract area(): number; } const s = new Shape('blob'); // Error: Cannot create an instance of an abstract class. ``` Note what an abstract class still is: a real class. It can declare fields, a constructor, static members, and fully implemented methods; it can itself extend another class. The constructor is not decorative — a subclass invokes it through `super(...)`, and the field assignments in it run for every instance of every subclass. The only thing taken away is the ability to write `new Shape(...)` against the abstract type. ## `abstract` on a member An abstract member is a **signature without an implementation**: ```ts abstract class Repository { abstract find(id: string): Promise<string>; // method abstract tableName: string; // property abstract get isReady(): boolean; // accessor } ``` The rules the compiler enforces around these: - An abstract member may appear **only inside an abstract class** — put one in a normal class and you get *Abstract methods can only appear within an abstract class*. - It may not carry a body. Adding one gives *Method 'find' cannot have an implementation because it is marked abstract*. - It cannot be combined with `static`, and it cannot be combined with `private` — an abstract member has to be visible to the subclass that implements it, so `protected abstract` and public `abstract` are the two useful forms. - An abstract property declaration produces no field at runtime. It only records that instances of the type have a member of that shape. ## What a subclass owes A **non-abstract** subclass must implement every abstract member it inherits, all the way up the chain: ```ts class Circle extends Shape { constructor(private radius: number) { super('circle'); } area(): number { return Math.PI * this.radius ** 2; } } ``` Omit `area` and the compiler says *Non-abstract class 'Circle' does not implement inherited abstract member 'area' from class 'Shape'*. The message names each missing member, so a class inheriting three unimplemented members reports three errors. A subclass that is itself declared `abstract` may leave some or all of them unimplemented and pass the obligation down — a common shape in layered hierarchies where a middle class implements the boring half of the contract and defers the interesting half. ## Why the feature exists: the template method shape The payoff is that the base class can write an algorithm **in terms of members it does not have yet**: ```ts abstract class Importer { run(rows: string[]): number { let ok = 0; for (const row of rows) { if (this.validate(row)) { this.persist(row); ok++; } } return ok; } protected abstract validate(row: string): boolean; protected abstract persist(row: string): void; } ``` `run` is inherited and reused; `validate` and `persist` are holes the compiler forces each subclass to fill. This is what makes an abstract class different from a bare shape declaration: the shared implementation and the mandatory hook ship together in a single type. The general design tradeoff — when a base class with holes beats composition — is language-agnostic and belongs to object-oriented design; what TypeScript adds is the compile-time guarantee that no subclass slips through with a hole left open. ## Type versus value The abstract class name is usable in both positions, but they behave differently. As a **type**, `Shape` names the instance type including the abstract members, so `function draw(s: Shape) { s.area(); }` type-checks fine and accepts any concrete subclass instance. As a **value**, the class binding exists but its constructor is not callable with `new`. That split is why a variable annotated `Shape` is ordinary and useful, while `new Shape(...)` is the error above. ## It is a compile-time promise Everything here is checked by `tsc` and then erased. The emitted JavaScript is an ordinary class, and nothing in it re-checks abstractness at runtime — so the guarantee is only as strong as the type checking around the call site.

  • Can an abstract class have a constructor, and if you can never call `new` on it, what runs that constructor?
    Yes, and it runs on every subclass instance. The subclass constructor calls `super(...)`, which executes the abstract class's constructor body — field assignments, argument validation, whatever it holds. `abstract` removes only the ability to construct the class directly; it does not make the constructor dead code.
  • What happens if a subclass of an abstract class implements only some of the inherited abstract members?
    It fails to compile unless the subclass is itself declared `abstract`. A non-abstract class must implement every inherited abstract member, and the compiler reports each missing one by name. Marking the intermediate class `abstract` is the legitimate way to implement part of the contract and pass the rest down.
  • Why does TypeScript reject `private abstract` on a member?
    A private member is invisible to subclasses, so no subclass could ever supply the implementation the modifier demands — the two modifiers ask for contradictory things and the compiler rejects the combination. When you want a hook that is internal to the hierarchy but still implementable, use `protected abstract`.

An abstract class is a form with some fields pre-filled and others left blank and marked required: you cannot submit the form as-is, and the office rejects any copy that comes back with a required blank still empty.

saying these in an interview costs you the question

  • Thinks an abstract class cannot contain implemented methods or state
  • Says the abstract class's constructor never runs
  • Believes abstract members are optional for subclasses to implement
  • Writes an abstract method with a body as a default implementation
  • Assumes `abstract` blocks instantiation at runtime too

context

open as a page

In a TypeScript class, what do the `public`, `private` and `protected` modifiers each control, and which code is allowed to touch a member marked with each?

level: juniorimportance: must knowfreq 70%

basics

~20 s

public, the default, allows access from anywhere. private limits access to the body of the declaring class. protected allows the declaring class and its subclasses. All three are checker-only rules that disappear when TypeScript emits JavaScript.

open as a page

In TypeScript, what is the difference between `class Duck implements Bird` and `class Duck extends Bird` — what does each clause change about type checking and about the emitted JavaScript?

level: juniorimportance: must knowfreq 78%

basics

~20 s

extends is real inheritance: the base class's members come along and the clause survives into the emitted JavaScript. implements is a compile-time-only conformance check that copies nothing and is erased. A class may implement many interfaces but extend only one class.

open as a page

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?

level: juniorimportance: must knowfreq 52%

basics

~20 s

The 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.

open as a page

In TypeScript, what does `constructor(private readonly repo: UserRepo) {}` declare on the class, and how is that different from `constructor(repo: UserRepo) {}`?

level: juniorimportance: must knowfreq 68%

basics

~20 s

An accessibility or readonly modifier on a constructor parameter makes it a parameter property: TypeScript declares a matching class member and assigns the argument to it. Without a modifier the parameter stays an ordinary constructor-local.

open as a page

In TypeScript, what does marking a class property `readonly` guarantee, and how is that different from declaring a variable with `const`?

level: juniorimportance: must knowfreq 72%

basics

~20 s

readonly is a compile-time-only restriction on writing a property: assignment is allowed just in the property's declaration or the declaring class's constructor. const restricts rebinding a variable instead. Neither stops mutation of the object inside.

open as a page

In TypeScript 5, a standard (TC39) decorator function is called with two arguments. What are they, and what does the function's return value do when it decorates a class method?

level: juniorimportance: must knowfreq 70%

basics

~20 s

A standard decorator receives the decorated value — the method, accessor or class itself — and a context object carrying kind, name, static, private, access and addInitializer. Whatever it returns replaces the decorated element; returning nothing leaves it untouched.

open as a page

In TypeScript, when would you model a contract as an abstract class rather than as an interface, and what does each choice cost?

level: middleimportance: must knowfreq 61%

basics

~20 s

Reach for an abstract class when implementers should inherit real code — shared method bodies, constructor logic, protected hooks — alongside the mandatory members. Reach for an interface when you only need a shape: it is erased, costs nothing at runtime, and any class can satisfy it.

open as a page

In TypeScript, what does marking a class member `private` actually prevent, and how is that different from declaring the member as `#name`?

level: middleimportance: must knowfreq 80%

basics

~20 s

TypeScript's private is a compile-time-only check that is erased on emit, so the property still exists and plain JavaScript can read it. A #name field is a real ECMAScript private field that stays unreachable from outside the class at run time.

open as a page

With experimentalDecorators enabled in TypeScript, a legacy method decorator is called with three arguments (target, propertyKey, descriptor). What is each argument, and what does returning a value from the decorator do?

level: middleimportance: must knowfreq 62%

basics

~20 s

TypeScript's legacy method decorator receives target (the prototype for an instance method, the constructor function for a static one), propertyKey (the member name), and the member's PropertyDescriptor. Returning a descriptor replaces the member's definition; returning undefined leaves it in place.

open as a page

In TypeScript, what does the `emitDecoratorMetadata` compiler flag actually emit, and what else has to be in place for a library to read that metadata at runtime?

level: middleimportance: must knowfreq 60%

basics

~20 s

For each decorated declaration, TypeScript emits design:type, design:paramtypes and design:returntype entries describing the declared types as runtime values. The flag works only alongside experimentalDecorators, and reading the entries requires the reflect-metadata polyfill imported once at startup.

open as a page

In TypeScript, a base class declares `where(sql: string): QueryBuilder` and `class UserQuery extends QueryBuilder` adds `withRoles()`. Why does `new UserQuery().where('id = 1').withRoles()` fail to compile, and what single change fixes it?

level: middleimportance: must knowfreq 62%

basics

~20 s

The annotation is the bug. where is declared to return QueryBuilder, so the chain collapses to the base type and withRoles is not on it. Declare the return type as this, the polymorphic this type, and the subclass survives the chain.

open as a page

TypeScript reports "Property 'name' has no initializer and is not definitely assigned in the constructor" on a class field. Which compiler check produces that error, and what are the legitimate ways to satisfy it?

level: middleimportance: must knowfreq 62%

basics

~20 s

The strictPropertyInitialization check produces it, and it needs strictNullChecks on. Satisfy it by giving the field an initializer, assigning it directly in the constructor body, including undefined in its type, or asserting with a definite-assignment !.

open as a page

In TypeScript with experimentalDecorators enabled, what is the difference between writing @log and @log('debug') above a class method, and how must the log function be written in each case?

level: juniorimportance: should knowfreq 50%

basics

~20 s

@log applies the log function itself as the decorator, so log must have the decorator signature. @log('debug') calls log first and applies whatever it returns, so log must be a factory: a function taking 'debug' and returning the actual decorator function.

open as a page

In TypeScript, `class User { name = ''; setName(n: string) { this.name = n; } }` makes `new User().setName('Ada').setAge(36)` fail to compile. What must a chainable method do, and what return type does the compiler then infer?

level: juniorimportance: should knowfreq 45%

basics

~20 s

Chaining requires the method to end with return this. With no return statement the method is inferred as returning void, and void has no members. Once it returns the receiver, TypeScript infers the polymorphic this type.

open as a page

In TypeScript, what does a first parameter literally named `this` declare in a function or method signature, and what does the compiler emit for it?

level: juniorimportance: should knowfreq 45%

basics

~20 s

A first parameter named this is a type-only declaration of the receiver the function requires. TypeScript checks call sites against it and then erases it: the emitted JavaScript has no such parameter and the function's arity is unchanged.

open as a page

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.

level: middleimportance: should knowfreq 54%

basics

~20 s

No. 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.

open as a page

In TypeScript, what does marking a class constructor `private` do, and how does a `protected constructor` differ?

level: middleimportance: should knowfreq 40%

basics

~20 s

A private constructor stops outside code from writing new C() and also stops the class from being extended, leaving static factory methods inside the class as the only way to build instances. A protected constructor blocks direct new but still permits subclasses.

open as a page

Code outside a TypeScript class writes `instance['secret']` to read a member declared `private secret: string`. Does that compile, and why does TypeScript allow it?

level: middleimportance: should knowfreq 36%

basics

~20 s

Yes, it compiles. TypeScript applies the private check to dot access only; element access with a string literal is a deliberate escape hatch, and it still resolves to the member's declared type rather than any.

open as a page

In TypeScript, given `interface Checker { check(name: string): boolean }` and `class NameChecker implements Checker { check(s) { return s.length > 0; } }`, why does the compiler complain about `s` when `noImplicitAny` is on?

level: middleimportance: should knowfreq 46%

basics

~20 s

An implements clause never supplies types. The class type is built from the class body first and only then compared to the interface, so the unannotated parameter is an implicit any — an error under noImplicitAny. Annotate it yourself.

open as a page

In TypeScript, `interface Options { retries: number; onError?: () => void }` and `class Config implements Options { retries = 3 }` — does that compile, and what goes wrong later when code calls `config.onError?.()`?

level: middleimportance: should knowfreq 38%

basics

~20 s

It compiles: an optional member is satisfied by absence. But the implements clause adds nothing to the class, so the type Config has no onError at all, and reading it through a Config-typed value is a compile error. Declare it explicitly.

open as a page

In TypeScript, a `class User {}` declaration puts two different things into scope under the same name. What are they, and what does `typeof User` mean when written in a type position?

level: middleimportance: should knowfreq 52%

basics

~20 s

A class declaration creates a value — the constructor object, holding the static members — and a type of the same name meaning an instance. In a type position, typeof User refers to that class value itself; InstanceType<typeof User> gets back the instance type.

open as a page

With experimentalDecorators enabled in TypeScript, what happens at runtime when a legacy class decorator returns a new constructor function, and why does the decorated class's static type not reflect the change?

level: middleimportance: should knowfreq 38%

basics

~20 s

Returning a constructor from a legacy class decorator replaces the class binding at runtime, so new instances come from the returned constructor — commonly a subclass that adds members. The compiler ignores the return: the class keeps the type it was declared with, so added members are type errors.

open as a page

In TypeScript with experimentalDecorators enabled, what arguments does a legacy parameter decorator receive, and why can it not inspect or change the argument value passed at call time?

level: middleimportance: should knowfreq 42%

basics

~20 s

A legacy parameter decorator is called with (target, propertyKey, parameterIndex) once, when the class is defined — propertyKey is undefined for constructor parameters. No call has happened yet and its return value is ignored, so it can only record the position for other code to use later.

open as a page

Almost all TypeScript syntax disappears at compile time. What does `constructor(private id: string) {}` leave behind in the emitted JavaScript, and why does that matter for tools that only strip types?

level: middleimportance: should knowfreq 46%

basics

~20 s

A parameter property emits real code: the compiler generates this.id = id; in the constructor body, plus a bare field declaration when class fields use define semantics. That makes it one of the few TypeScript features a type-stripping tool cannot handle.

open as a page

In a TypeScript class, what does declaring a method as `isDirectory(): this is Directory` give you that declaring it `isDirectory(): boolean` does not?

level: middleimportance: should knowfreq 32%

basics

~20 s

A this-based type predicate narrows the receiver: inside a branch where the call returned true, the compiler treats the object the method was called on as a Directory. A plain boolean return narrows nothing at all.

open as a page

In TypeScript, `readonly` on a class property is often called an unenforceable promise. What are the concrete ways a value can still be written despite it?

level: middleimportance: should knowfreq 44%

basics

~20 s

Four routes: the modifier is erased, so untyped code writes freely; assignability ignores readonly, so a mutable-typed alias can write through; a type assertion strips it; and it is shallow, so the object it points at stays mutable.

open as a page

In a TypeScript class body, what does declaring a member with the `accessor` keyword — `accessor name = 'ann'` — actually create, and what does a standard (TC39) decorator on it receive and return?

level: middleimportance: should knowfreq 38%

basics

~20 s

An auto-accessor creates a getter/setter pair backed by hidden private storage instead of a plain data property. Its decorator receives an object with get and set functions and may return replacements plus an init function that transforms the starting value.

open as a page

In TypeScript 5, a standard (TC39) decorator placed on a class field is called with `undefined` as its first argument. Why, and what can such a decorator actually change about the field?

level: middleimportance: should knowfreq 45%

basics

~20 s

A field has no value when the class is defined, so nothing can be passed. A field decorator instead returns an initializer transform, a function taking each instance's initial value and returning the value actually stored, with this bound to the instance.

open as a page

What does TypeScript's `noImplicitThis` compiler option report, and why do class methods and object-literal methods usually keep compiling when you turn it on?

level: middleimportance: should knowfreq 50%

basics

~20 s

noImplicitThis reports every use of this whose type would otherwise fall back to any. Class methods and object-literal methods are unaffected because the checker already knows their receiver, so only free-standing functions that read this must be annotated.

open as a page

showing 1–30 of 46