← back to section

When data and behaviour grow together, it is convenient to collect them into a single type with clear rules. TypeScript provides classes for this — and on top of them sit decorators, which power frameworks like Angular and NestJS. Let's cover both from the very beginning.

Why classes exist

A class is a template for creating objects. It describes what fields (data) and methods (behaviour) an object will have, and brings everything together in one place.

JavaScript also has classes, but TypeScript adds types and access modifiers — checks that catch errors before the code even runs.

class User {
  name: string; // field and its type
  age: number;

  constructor(name: string, age: number) {
    this.name = name; // constructor fills in the fields
    this.age = age;
  }

  greet(): string {
    return `Hello, I'm ${this.name}`; // method
  }
}

const u = new User("Ann", 30); // create an object (instance)
console.log(u.greet()); // Hello, I'm Ann

Short formula: a class describes the shape; new creates a concrete object from that shape.

Fields and the constructor

The constructor is a special method called when new is used. Its job is to prepare the object: accept input data and store it in fields.

TypeScript can reduce boilerplate. If you add an access modifier directly to a constructor parameter, the field is both declared and assigned automatically:

class User {
  // public name + public age are created and assigned automatically
  constructor(public name: string, public age: number) {}
}

const u = new User("Ann", 30);
console.log(u.name); // Ann

A field can be marked readonly — it can then only be set in the constructor; changing it afterwards is not allowed:

class Order {
  constructor(readonly id: string) {}
}

const o = new Order("A-1");
// o.id = "A-2"; // compile error: id is read-only

Access modifiers

Access modifiers control who is allowed to access a field or method. There are three:

  • public — accessible everywhere (this is the default).
  • private — accessible only inside the class itself.
  • protected — accessible inside the class and its subclasses.
class Account {
  private balance = 0; // not visible from outside

  deposit(amount: number): void {
    this.balance += amount; // allowed inside the class
  }

  getBalance(): number {
    return this.balance;
  }
}

const a = new Account();
a.deposit(100);
// a.balance; // error: balance is private
console.log(a.getBalance()); // 100

Important to understand: private and protected are compile-time checks. They guard against accidental mistakes in code, but in the compiled JavaScript the field is still accessible. For true runtime privacy, use the JavaScript # syntax:

class Account {
  #balance = 0; // runtime-level privacy

  deposit(amount: number): void {
    this.#balance += amount;
  }
}

Inheritance

Inheritance lets you create a class based on another, reusing its fields and methods. A subclass is declared with extends, and the parent is accessed via super.

class Animal {
  constructor(protected name: string) {}

  describe(): string {
    return `This is ${this.name}`;
  }
}

class Dog extends Animal {
  constructor(name: string, private breed: string) {
    super(name); // calling the parent constructor is required
  }

  describe(): string {
    // override the method, but reuse the parent's version
    return `${super.describe()}, breed ${this.breed}`;
  }
}

const d = new Dog("Rex", "shepherd");
console.log(d.describe()); // This is Rex, breed shepherd

When a subclass provides its own version of a method with the same name, that is called overriding. Since TypeScript 5 you can mark such a method explicitly with the override keyword — the compiler will verify that the parent actually has a method by that name.

Abstract classes

An abstract class is a template from which you cannot create an object directly. It defines a common skeleton and leaves some methods unfilled (abstract), requiring subclasses to implement them.

abstract class Shape {
  abstract area(): number; // no body — subclass must implement it

  describe(): string {
    return `Area: ${this.area()}`; // shared method is already ready
  }
}

class Circle extends Shape {
  constructor(private radius: number) {
    super();
  }

  area(): number {
    return Math.PI * this.radius ** 2;
  }
}

// new Shape(); // error: cannot instantiate an abstract class
const c = new Circle(2);
console.log(c.describe()); // Area: 12.566...

An abstract class is useful when a group of classes shares common logic but each one implements one or two details its own way.

Classes and interfaces

An interface describes a contract — what fields and methods an object must have, without implementing anything. A class declares that it honours the contract via implements:

interface Repository {
  save(value: string): void;
  findAll(): string[];
}

class InMemoryRepository implements Repository {
  private items: string[] = [];

  save(value: string): void {
    this.items.push(value);
  }

  findAll(): string[] {
    return this.items;
  }
}

If a class omits any method from the interface, the compiler flags it immediately. The difference from an abstract class: an interface is only a shape description (no code, no state), while an abstract class can carry ready-made logic and fields. A class can inherit one class but may implement multiple interfaces.

Decorators: what they are and why

A decorator is a function that is "attached" to a class, method, field, or parameter via the @DecoratorName syntax. It receives what it is attached to and can add behaviour or metadata — without manually changing the source code.

Writing decorators from scratch is something a beginner almost never needs to do. But you encounter them immediately as soon as you pick up a framework. The canonical examples are Angular on the frontend and NestJS on Node — decorators are everywhere there. Here is what it looks like in NestJS:

@Controller("users") // the class becomes a controller for the path /users
class UsersController {
  @Get(":id") // the method handles GET /users/:id
  findOne(@Param("id") id: string): string {
    return `User ${id}`;
  }
}

Here @Controller, @Get, and @Param are decorators. They do not handle the request themselves; they mark the class and method with metadata: "this is a controller", "this method responds to GET", "take the id parameter from the URL". The framework reads these marks and builds routing from them. In other words, a decorator is a way to express intent concisely and declaratively, delegating the boilerplate to the framework.

How to enable decorators

Decorators in the NestJS ecosystem use an earlier version of the feature, so they must be explicitly enabled in tsconfig.json:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}
  • experimentalDecorators — enables the @ syntax itself.
  • emitDecoratorMetadata — adds type information to the compiled output; NestJS uses it to understand which dependencies to inject.

Worth knowing: TypeScript 5 introduced standard decorators (following the new language specification) — they work without the experimentalDecorators flag. But they have a different format, and frameworks like NestJS still rely on the "experimental" version. So when you add a ready-made framework, simply set the flags required by its documentation.

In short

  • A class bundles data and behaviour into one type; new creates an object from it.
  • The constructor prepares the object; a modifier on a parameter automatically declares and fills the field; readonly prevents changing it after creation.
  • Access modifiers public/private/protected are compile-time checks; for true runtime privacy use the # syntax.
  • extends provides inheritance (call the parent via super); abstract defines a skeleton that cannot be instantiated directly.
  • interface + implements describe a contract without code; a class implements multiple interfaces but inherits one class.
  • A decorator is an annotation function via @; NestJS is built on them (@Controller, @Get).
  • For NestJS decorators, enable experimentalDecorators and emitDecoratorMetadata in tsconfig.json.
  • TypeScript type basics — the foundation on which class fields and interfaces rest.
  • Generics — how to make classes and methods reusable for different types.
  • Tooling and tsconfig — where flags like experimentalDecorators live and how to configure the build.