What’s the problem with TypeScript’s classes?

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

TypeScript’s classes

If you wonder why this code compiles:

class User {  constructor(public name:string) {}}function isUser(user: User): boolean {  return user instanceof User; // should always return true?}// compiles even though we didn't do `new User`isUser({name: 'Georges'}); // return false

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:

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

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:

interface Car { brand: string }interface Bike { brand: string, id: number }
function getBrand(car: Car): string { return car.brand;}const bike: Bike = {id: 1, brand: 'Decathlon'};getBrand(bike) // compile because `Bike` has the attribute `brand`

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:

class User {  constructor(public name:string) {}}
// here it's the type `User`
function isUser(user: User): boolean { return user instanceof User; // here it's the value `User`}isUser({name: 'Georges'}); // return false

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:

class User {  constructor(private name:string) {}}function isUser(user: User): boolean {  return user instanceof User;}// The declaration identical as Userclass Alien {  constructor(private name:string) {}}// Won't compile - the error is:// Types have separate declarations of a private property 'name'isUser(new Alien('Paul'));

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.

export function getUserById(id:string): User { //...

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.

export const UserService =  {  getUserById() // ....}

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

// user.service.ts
export function getUserById(id:string): User { //...
// another file
import * as UserService from 'user.service.ts';
UserService.getUserBy('123');

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.

class User {  private _tag: 'User' = 'User';  constructor(private name:string) {}}

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

--

--

--

Empowering The Sport Tech Community

Recommended from Medium

What is Node.js?

Pro Tip — Developing on Windows: Setting variables in your script command

Introduction of JavaScripts and its Basics

Global and Local variables in javaScript

Why choose Nest.js as your backend framework?

Typescript: Are “Private” properties private?

How To Remove Duplicate Code Properly From Your App

duplicate security cameras looking in the same direction

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Benoit Lemoine

Benoit Lemoine

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

More from Medium

An interesting use case of advanced typescript

Don’t use method syntax for your TypeScript function types!

TypeScript vs. JavaScript: Which should you use?

Typescript + React — a retrospective (TL;DR — it’s awesome!)

Eagle’s head attached to dog’s legs