What’s the problem with TypeScript’s classes?

Values, Types, and the counter-intuitive behavior of classes

Benoit Lemoine
Decathlon Digital
3 min readJan 17, 2021

--

TypeScript’s classes

If you wonder why this code compiles:

this article will explain it, and will provide some guidelines on how and when to use classes in TypeScript.

Values and Types

TypeScript handles values — things that will be present at runtime — and types — constraints on value that exist only at compile time. Those concepts live in two separate worlds that can’t communicate (there are exceptions, but they lie outside the scope of this article).

For example, const a = 1 declares a value with the name a, function addOne(n: number): number { n + 1 } declares a value with the name addOne. On the other side type Car = { brand: string } declares a type with the name Car , or interface Website { url: string } declares a type with the name Website.

The keyword class is peculiar here, because it defines both a type AND a value:

defines a value named User, that can be instantiated with the new keyword: const u = new User(‘Georges’).

But it also defines a type named User, defined as “something with a name attribute that should be a string”.

Structural Typing

TypeScript is structurally typed. It means that, as long as the structure of two types can be unified, the code will compile:

This behavior is profoundly different than what we can observe in most other object oriented languages (Java, C# , etc.), which are often nominally typed: they not only check their structure, they also check that the names of the types are matching.

With this information, we now understand why our first example compiles without error:

Nominal typing emulation

To enforce the fact that value is an instance of a given class, we can abuse TypeScript’s behavior pertaining to `private` attribute:

When should I use classes?

TypeScript’s best practice matches most other languages’: a class should have attributes, and those attributes should be private.

But you may argue the following:

What if I have a utility class — only methods, not attributes?

You can export those functions directly; you don’t need a class.

But what if I want to namespace the functions?

You can export an object instead of a class, and you won’t need to call `new` on it.

Or you can namespace on import and keep simple function export:

What if I want the syntactic sugar of methods?

If you want to be able to use the dotted notation, and you don’t want to set the attribute to private for whatever reason, the easiest way to avoid any potential problem is to add a private tag attribute. It’s only there to ensure TypeScript will use “nominal typing” on your class.

In all other cases, encapsulation of the class state should be the way to go.

All code was tested with TypeScript 4.1

Thank you for reading!

🙏🏼 If you enjoyed the article, please consider giving it a few 👏👏👏.

💌 Learn more about tech products opened to the community and subscribe to our newsletter on http://developers.decathlon.com

--

--

Benoit Lemoine
Decathlon Digital

I’m a full-stack developer, in love with functional programming and type systems. I’m working currently at Decathlon Canada, in Montreal QC.