---
title: "11 — Classes and Decorators: OOP and Metaprogramming in TypeScript"
uid: classes-decorators
tags: ["oop", "access-modifiers", "classes", "typescript", "decorators", "roadmap:typescript", "inheritance"]
excerpt: "Classes package data and behavior with access control; decorators are functions, written as @expression, that annotate or modify class declarations when the class is evaluated."
date: 2026-08-13T03:27:27+0000
source: https://www.aveshina.my.id/en/blog/classes-decorators
---

"JavaScript classes with extra keywords" was my classes-and-decorators model, and it fused two different ideas. Writing it down separated them: **classes package related data (properties) and behavior (methods) into a reusable unit with access control, and decorators are functions, written as @expression, that annotate or modify class declarations when the class is evaluated.** [1][2] They're related — decorators attach to classes — but they solve different problems.

The starting claim is that a class is a blueprint for objects. It defines the properties and methods that every instance of the class will have, so I can create many objects with the same structure from one definition [1]:

```
class User {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  greet() {
    return "Hello, " + this.name;
  }
}
```

Every new User("Ave") produces an object with a name property and a greet method. The class is the template; the instances are the products.

## Constructor parameters and the parameter property shorthand

TypeScript offers a concise way to declare *and* initialize a property in one stroke. Putting an access modifier (public, private, or protected) on a constructor parameter tells the compiler to create a matching property and assign the argument to it [3]:

```
class User {
  constructor(public name: string) {}
}
```

This single line declares a name property, accepts it in the constructor, and assigns it — no separate field declaration, no this.name = name. It's a small but real productivity win for classes with many fields, and it's the idiomatic way to write constructors in TypeScript.

## Access modifiers: controlling visibility

**Access modifiers** control where a class member can be accessed from [4]:

- **public** — accessible anywhere. The default; no modifier means public.
- **private** — accessible only inside the class. The compiler enforces this at build time (it's not a runtime lock).
- **protected** — accessible inside the class *and its subclasses*.

These bring encapsulation to TypeScript classes: internal implementation details can be hidden behind a private marker, exposing only the public surface that callers should rely on. The TypeScript-specific #field syntax (from JavaScript) provides true runtime privacy; the private keyword provides compile-time-only privacy. The two coexist, and the choice is about whether I need the guarantee to survive to runtime.

## Abstract classes and inheritance

An **abstract class** is a blueprint that can't be instantiated directly — it's meant to be subclassed [5]. It can contain abstract methods (declarations with no body) that subclasses must implement, plus concrete methods they inherit:

```
abstract class Animal {
  abstract sound(): void; // subclasses must implement
  breathe() { console.log("breathing"); }
}
```

Animal can't be new'd, but a class Dog extends Animal can, provided it implements sound(). Abstract classes define a shared contract and shared behavior in one place, which is the essence of **inheritance** — a subclass acquires the properties and methods of its parent, an "is-a" relationship.

## Inheritance vs. polymorphism

**Inheritance** is the mechanism — one class derives from another. **Polymorphism** is the consequence: objects of different subclasses can be treated as instances of a common base type, and each responds to the same method call in its own way [6]. If Dog and Cat both extend Animal and both implement sound(), I can hold an array of Animal[] and call .sound() on each — the right implementation runs for each object without me knowing its concrete type. That's polymorphism, and it's the payoff of the inheritance hierarchy: code written against the base type works for every subtype.

## Method and constructor overloading

**Method overriding** lets a subclass replace an inherited method with its own implementation [7]. When a subclass defines a method with the same name as the parent's, calls on the subclass run the subclass's version. (TypeScript now requires the override keyword to make this explicit, which catches the case where the parent's method is renamed but the subclass's override is left dangling.)

**Constructor overloading** in TypeScript follows the function-overloading pattern: multiple signatures, one implementation. A class can declare several constructor signatures to accept different argument shapes, all funneled into a single implementation body. It's less common than method overriding but useful when a class can be constructed from different kinds of input.

## Decorators: annotating declarations

**Decorators** are a different feature entirely. They're a syntax for attaching a function to a class, method, accessor, property, or parameter using the @expression form, where expression evaluates to a function called at runtime with information about the decorated declaration [2]. A decorator can observe, replace, or augment what it's attached to:

```
@Component({ selector: "my-card" })
class CardComponent {}
```

Here @Component(...) is a decorator — a function that receives the class and registers it with a framework. Decorators are how libraries like NestJS, TypeORM, and Angular build their declarative APIs: instead of wiring up classes imperatively, I annotate them and the framework reads the annotations at runtime. They're opt-in (require the experimentalDecorators compiler option, or the newer ECMAScript decorators stage), and they're most valuable in ecosystems that consume them. Outside those ecosystems I rarely write my own.

## How I use this

My defaults: reach for classes when I have related state and behavior that belong together, especially with access control — and use the parameter-property shorthand to keep constructors terse. Use private/protected to hide internals and expose a deliberate public surface. Model shared contracts with abstract classes and lean on polymorphism to write code against base types. Reach for **decorators** only inside a framework that consumes them (NestJS, TypeORM, Angular) — they're powerful but their value is ecosystem-dependent, and rolling my own is rarely worth it. The discipline that pays off is keeping the two ideas separate in my head: classes organize object structure; decorators annotate declarations — and confusing them leads to muddled designs.

## References

[1] Microsoft, "Classes," TypeScript Handbook, 2024. [Online]. Available: [https://www.typescriptlang.org/docs/handbook/2/classes.html](https://www.typescriptlang.org/docs/handbook/2/classes.html)

[2] Microsoft, "Decorators," TypeScript Handbook, 2024. [Online]. Available: [https://www.typescriptlang.org/docs/handbook/decorators.html#handbook-content](https://www.typescriptlang.org/docs/handbook/decorators.html#handbook-content)

[3] Microsoft, "Constructors," TypeScript Handbook, 2024. [Online]. Available: [https://www.typescriptlang.org/docs/handbook/2/classes.html#constructors](https://www.typescriptlang.org/docs/handbook/2/classes.html#constructors)

[4] TypeScript Tutorial, "TypeScript Access Modifiers," 2024. [Online]. Available: [https://www.typescripttutorial.net/typescript-tutorial/typescript-access-modifiers/](https://www.typescripttutorial.net/typescript-tutorial/typescript-access-modifiers/)

[5] Microsoft, "Abstract Classes and Members," TypeScript Handbook, 2024. [Online]. Available: [https://www.typescriptlang.org/docs/handbook/2/classes.html#abstract-classes-and-members](https://www.typescriptlang.org/docs/handbook/2/classes.html#abstract-classes-and-members)

[6] Dev.to, "Mastering Object-Oriented Programming with TypeScript," 2023. [Online]. Available: [https://dev.to/rajrathod/mastering-object-oriented-programming-with-typescript-encapsulation-abstraction-inheritance-and-polymorphism-explained-c6p](https://dev.to/rajrathod/mastering-object-oriented-programming-with-typescript-encapsulation-abstraction-inheritance-and-polymorphism-explained-c6p)

[7] Microsoft, "Overriding Methods," TypeScript Handbook, 2024. [Online]. Available: [https://www.typescriptlang.org/docs/handbook/2/classes.html#overriding-methods](https://www.typescriptlang.org/docs/handbook/2/classes.html#overriding-methods)

```quiz
Q: Putting `public name: string` on a constructor parameter does what?
- declares a property named name, accepts it as an argument, and assigns it — all in one line
- nothing special; it's just a typed parameter
correct: 0
explain: This is the parameter property shorthand. An access modifier on a constructor parameter tells TypeScript to create and assign a matching property automatically.

Q: The `private` modifier in TypeScript provides privacy…
- at runtime, enforced by the JS engine
- at compile time only — the compiler blocks access; there's no runtime lock
correct: 1
explain: The private keyword is a compile-time check. For true runtime privacy, use the #field syntax (JavaScript native private fields).

Q: An abstract class is one that…
- can be instantiated but has no methods
- cannot be instantiated directly; it's a blueprint meant to be subclassed, possibly with abstract methods subclasses must implement
correct: 1
explain: Abstract classes define shared contracts and behavior. They can declare abstract methods (no body) that subclasses must implement, plus concrete methods they inherit.

Q: Polymorphism means…
- subclasses replace inherited methods
- objects of different subclasses can be treated as instances of a common base type, each responding to the same call in its own way
correct: 1
explain: Polymorphism is the consequence of inheritance — code written against a base type works for any subtype, with each subtype's implementation running.

Q: A decorator written as `@expression` is, mechanically…
- a compile-time annotation that produces no runtime effect
- a function, evaluated at runtime, called with information about the decorated declaration
correct: 1
explain: The @expression form evaluates expression to a function that runs at runtime when the class is evaluated, receiving details about what it decorates. Frameworks use this to register or modify classes declaratively.
```
