Is TypeScript Object-Oriented? A Practitioner's Answer

 Is TypeScript Object-Oriented? A Practitioner's Answer

Ask this question on a search engine and you will be told, confidently, that yes, TypeScript is object-oriented. Technically that answer is correct. Practically, it is the least useful answer you can give, because it hides the actual decision every TypeScript developer makes multiple times a day: should this piece of code be a class at all?

I build products in TypeScript every week. The backend of AmplyDigest, my daily newsletter-summarization service, runs on Cloudflare Workers in TypeScript. The interactive 3D property viewer my studio sells, AmplyViewer, is a TypeScript module embedded into real estate websites. In both projects, I have made the call to keep certain subsystems class-heavy and to strip classes out of others entirely. That experience is what I want to share here, because the textbook answer does not help you ship anything.

The short answer, and why it is misleading

TypeScript supports every feature you would want from an object-oriented language. You get classes with constructors, instance and static methods, access modifiers like private, protected, and public, abstract classes, interfaces, generics, and even decorators behind a flag. That covers the four pillars of OOP - encapsulation, inheritance, polymorphism, and abstraction - more completely than vanilla JavaScript ever did.

So if the definition of an object-oriented language is "can you write object-oriented code in it," then yes, obviously. But that is a yes-or-no exam question, and nobody ships software by answering exam questions. The real question is how TypeScript behaves when you actually use OOP, and how often OOP is the right paradigm inside modern TypeScript stacks. Those two questions have much more interesting answers.

class-based syntax

Where TypeScript stops behaving like classical OOP

Here is the detail that changes how I write TypeScript: the type system is structural, not nominal. That distinction matters enormously. In a nominal language like Java or C#, two classes with identical fields and methods are still different types. You cannot pass one where the other is expected. In TypeScript, if two objects have the same shape, they are the same type - regardless of whether you went through the ceremony of declaring a class.


class Rectangle {
  constructor(public width: number, public height: number) {}
  area() { return this.width * this.height; }
}

const somethingShapedLikeOne = {
  width: 10,
  height: 5,
  area() { return this.width * this.height; },
};

function render(r: Rectangle) { /* ... */ }
render(somethingShapedLikeOne); // compiles fine

That snippet would not compile in Java. In TypeScript, it is expected behavior, and the official TypeScript handbook on type compatibility explains why: the language was designed to describe JavaScript code as it is actually written, and JavaScript is full of anonymous objects and duck-typed patterns. Forcing nominal typing onto that would make the type system useless.

The practical consequence is that a lot of the protective guarantees you expect from OOP in Java do not exist in TypeScript. A Rectangle is not a unique, closed category - it is a shape description. This is why senior TypeScript developers reach for "branded types" when they genuinely need nominal distinctions, like keeping a UserId separate from a ProductId even though both are strings. It is also why copying OOP patterns straight from a Java or C# codebase into TypeScript often produces code that looks right but does not buy you the safety you assumed.

ECMAScript

What my actual projects look like

Most of my production TypeScript code is not class-based. It is typed functions, typed records, and narrow composition. That is not ideology - it is what works inside the tools I ship with.

The AmplyDigest backend runs Hono on Cloudflare Workers. Workers are short-lived, cold-start-sensitive, and spawn per request. The moment you start wiring up dependency-injected service classes the way you would in a long-running NestJS process, you are paying for patterns the runtime does not reward. A handler that takes a request and returns a response is a function. I type the inputs and outputs, compose a couple of pure helpers, and move on. When I looked at what was genuinely improving with classes, it was mostly ceremony.

The AmplyViewer module is different. It manages 3D scene state - cameras, meshes, selection, interaction. That is legitimately stateful, long-lived, and has clear lifecycle concerns. I use classes there because they map to real objects in the real domain. The internal render loop is a class, the input handler is a class, the scene registry is a class. Each one owns state and exposes a small surface. In that context, the four pillars earn their keep.

On the frontend side of the studio - the real estate websites I build for clients on Astro and Svelte - components are almost always functions. Svelte compiles component authoring into plain functions and reactive primitives. Astro's island model discourages long-lived class hierarchies on the client. If you check the TypeScript handbook entry on object types, you will notice the examples there lean heavily on interfaces and type aliases rather than classes. That reflects how most TypeScript teams actually model data today.

So when do I reach for classes

There are cases where class syntax still earns its place in my TypeScript work. I keep the criteria short because the list grows fast if you let it:

  • The thing you are modeling has a long lifecycle and owns state that evolves over time - a WebSocket session, a scene graph node, a retry-aware queue consumer.
  • You have a framework that is genuinely class-oriented and rewards following its conventions, such as NestJS with its decorators and dependency injection system.
  • You need a clean abstract contract with multiple concrete implementations and the ergonomics of instanceof checks matter for your runtime logic.
  • You are writing a library where the class is part of the public API shape consumers expect, like a game engine or a 3D toolkit.

Everything else tends to start as functions and records. If a file starts collecting five loose functions that all take the same shape of object, that is sometimes a signal to promote it into a class. Usually it is a signal to define an interface and keep composing.

Complete OOP Adoption

Where OOP hurts in modern TypeScript stacks

The honest warning: class-first thinking is a common way for TypeScript codebases to get expensive. Deep inheritance chains in a structurally-typed world give you almost none of the guarantees they give in Java, while making refactoring noticeably harder. Singletons built with private constructors cooperate poorly with serverless runtimes where module scope does not persist the way you expect. Heavy use of decorators requires keeping a specific TypeScript config flag on, which couples your project to a syntax whose stable behavior has moved around more than the rest of the language.

If you are coming to TypeScript from Java or C# and you treat it as "Java with JavaScript syntax," you will write fluent-looking code that fights the runtime. If you come from JavaScript and treat TypeScript as "typed JavaScript that happens to also support classes," you will end up closer to how most large TypeScript codebases actually look. The TypeScript best-practice patterns we follow in the studio lean in that direction for exactly this reason.

How this shapes the way I teach the language

When a junior developer asks me if TypeScript is object-oriented, I answer in two parts. The first part is yes, here is what the language supports, here is how class, interface, and abstract work. The second part is the more important one: use OOP when you have real objects to model, not as the default way to organize code. TypeScript rewards thinking in data and shapes first and behavior second. That is closer to how the JavaScript runtime actually behaves underneath, and closer to how modern frameworks expect you to build.

The flip side is that TypeScript does not punish you for writing classes when they earn their place. That flexibility is the reason it fits comfortably into both a class-heavy NestJS backend and a function-first Svelte frontend without splitting into two dialects. It is a multi-paradigm language that happens to include a strong OOP toolkit, not an OOP language with functional decorations. Understanding that order is what separates practitioners who ship clean TypeScript from developers who fight the compiler every week.

If you are making a platform decision around TypeScript rather than a style decision, the broader context matters too - you can see how I break that down in my piece on whether TypeScript is a frontend or backend language, and the TypeScript versus JavaScript comparison covers why teams usually adopt it in the first place.

Enhanced Private Fields

Frequently asked questions

Is TypeScript truly object-oriented like Java or C#?

No. TypeScript has the full surface of OOP features - classes, inheritance, access modifiers, abstract classes - but its type system is structural rather than nominal. Two classes with identical members are interchangeable in TypeScript and would not be in Java or C#. That one difference cascades into how patterns behave in practice, which is why copying idioms directly from Java tends to produce code that compiles but does not protect you the way you expected.

Can TypeScript be used without classes at all?

Yes, and a large portion of production TypeScript is written this way. Typed functions, interfaces, and type aliases cover the majority of use cases. Frontend frameworks like Svelte and React lean functional, and serverless runtimes like Cloudflare Workers do not reward class-heavy architectures. Classes remain useful for long-lived stateful objects and for frameworks that depend on them, such as NestJS.

Why does TypeScript use structural typing instead of nominal?

Because JavaScript is written that way. JavaScript code is full of anonymous objects, object literals, and duck-typed interfaces. A nominal type system would have forced developers to wrap every existing pattern in class ceremony before TypeScript could describe it. Structural typing lets TypeScript describe real JavaScript code without rewriting it, which is why adoption was as smooth as it was.

Should I prefer classes or interfaces when modeling data in TypeScript?

Default to interfaces and type aliases when you are describing shape. Reach for classes when the thing you are modeling has behavior, owns state over time, or needs to be instantiated as a first-class runtime object. A good rule of thumb is that if the only methods on your class are getters or pass-throughs, it probably wanted to be an interface.

Do decorators make TypeScript more object-oriented?

They add syntactic support for annotating classes and members, which makes class-first frameworks like NestJS more ergonomic. They do not change the underlying type system, and they are not required for most application code. Turning on decorators also couples your project to a specific TypeScript config and, historically, to a syntax that has evolved more than the rest of the language. I only enable them when the framework I am using truly depends on them.

Is object-oriented TypeScript slower than functional TypeScript?

Rarely because of OOP itself. Performance differences usually come from architectural choices, such as holding singletons across serverless invocations or building deep inheritance trees that fight tree-shaking. In the runtimes I ship to - Cloudflare Workers, browser bundles, Node servers - well-written class-based and function-based TypeScript end up in the same performance neighborhood. What kills performance is ceremony, not paradigm.