AV
HomeAboutProjectBlog

© 2026 Ave syah Shina. All rights reserved.

  1. Home
  2. Blog
  3. 11 — Classes and Decorators: OOP and Metaprogramming in TypeScript

11 — Classes and Decorators: OOP and Metaprogramming in TypeScript

August 13, 20266 min read
Download as Markdown

"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

[2] Microsoft, "Decorators," TypeScript Handbook, 2024. [Online]. Available: 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

[4] TypeScript Tutorial, "TypeScript Access Modifiers," 2024. [Online]. Available: 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

[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

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

Knowledge check · Question 1 of 5

Putting `public name: string` on a constructor parameter does what?

Comments

Leave a Comment

You must be signed in to comment

0 Comments

No comments yet. Be the first to comment!