TypeScript: the Scala Perspective

Dmytro Naumenko
Wix Engineering
Published in
7 min readJan 25, 2021

--

TL;DR: In the blog post below I give a quick overview of the things I learned from writing production TypeScript code at Wix, discuss what TypeScript and Scala have in common, and show how to do some FP/Scala routine tasks the TypeScript way. If you’re a Scala developer, reading this will give you an intro into what TypeScript is capable of, how you can easily switch to using it, and how to become comfortable with it.

It is fair to say that this is quite a large topic, so I’ll only focus on things I wish I knew when I started, plus give a summary with my general impressions.

Null

First things first, there is a way to tell the TypeScript compiler to be strict about null and undefined types — via the strictNullChecks compiler option. So the first thing you need to do is enable it (or even better, enable the strict mode with all static checks).

In this case, null will be a valid value only for any type. It means you can’t define const a: number = null and need to specify null as valid options explicitly:

const a: number | null = null

But keep in mind, it’s more convenient to use optional parameters and fields in some places and not declare fields as nullable. If you have a method with signature: function foo(bar: int | null), usually you don’t distinguish between null and undefined in your code, so it can be safely replaced with function foo(bar?: int) instead. The same is true for field declarations in your interfaces.

Note: In the JavaScript world, there is a difference between `undefined` and value being `null`. The first one is for cases when a variable was defined, but no value given. The second is when someone sets value to null. One use case where it can be handy is partial updates. Let’s imagine you want to update only the name of Person and keep all other fields as is. So you just pass it as `{name: “New Name”}` in JSON. If you want to remove age from Person completely, you can pass the null value directly: `{name: “New Name”, age: null}`. In Scala, you will either set the age to 0 or -1, or introduce a new type and represent it as something like `Option[Option[Val]]` where the first option indicates if value was present and second if it was set to null or not.

Subtyping

Scala supports both Nominal and Structural Subtyping.

Nominal subtyping is well-known to Java or C# devs, there it’s the only way to define a particular type as a subtype of another. It’s called nominal because you have to define/name subtyping relations explicitly, e.g. `trait A extends B`.

But Scala has another option — “refinement type”. The famous textbook example is a definition of “an animal that eats grass” as an `Animal { type suitableFood = Grass }`.

Although this is a powerful feature, I seldomly see it used in production Scala code. Only several example of it being used I remember are:

  • getting type-lambdas via kind-projector
  • refined library which allows building type-level validators via Shapeless

Anyway, raw refinement types are hidden from your code in the examples above. In TypeScript, any type (except primitive) is a refinement type and refinement types are your breed and butter. They are everywhere and it’s much easier to write a structural type instead of a usual class. The syntax for refinement type in TypeScript is just an interface definition:

interface Animal { 
suitableFood: "grass"
}

Other options are classes (but more verbose) and type aliases for object literal (i.e. type Animal = { … }). Interface declaration is shorter than classes and gives you better error explanations.

So it is preferred to use TypeScript interfaces in any situation where you would rely on case-classes in Scala. One thing which will help in the future regarding interfaces is that we can have a kind of exhaustive checks for Pattern Matching. We’ll see this in action a little bit later.

Union/Intersection Types

In Scala 2, we can simulate union types with sealed traits:

sealed trait Or
trait Left extends Or
trait Right extends Or

In TypeScript and Scala 3, you can just write type Or = Left | Right.

I found it useful in declaring in-place enums, as it has shorter syntax. In another case I tried to use them for error handling — i.e. defining that function can return either result or error, but it didn’t work well. The main problem is that you don’t have any means of combination or common functionality for handling errors in this case. We’ve got Try and Either in Scala and you can chain them with for-comprehension, but in TypeScript, you had to do manual checks.

The intersection types are different beasts. Scala 3 will get them as well. I found them useful for sticking to DRY and splitting objects with a lot of fields into multiple types. For example let’s imagine a CRUD API with type Entity = EntityId & EntityData . Here, the save method can accept only entity data (assuming that EntityId is auto-generated), and the update method will accept the whole Entity. In Scala, you will either have a duplication or will need to make EntityData nested inside of Entity.

Pattern Matching

In combination with singleton literal types, TypeScript allows you to get a pattern matching exhaustive checks. TypeScript developers call it discriminator types and the syntax is a bit awkward.

interface A { kind: "A", propA: string }
interface B { kind: "B", propB: string }
function foo(param: A | B): string {
switch (param.kind) {
case "A":
return 'foo'
case "B":
return 'bar'
}
}

Note that you will only get an exhaustive check if you use — strictNullChecks and specify the return type. TypeScript will complain that return types don’t match if you forget to include all possible options in the switch statement.

Mapped types

It’s a neat feature that allows you not to repeat yourself and create new types based on the old ones with some changes.

I found it useful in different DTO mappings — you can remove the burden of re-declaring the type by using a specific syntax for type mapping. This is really hard to do in Scala. But in TypeScript, creating a new type with all fields being optional is a one-liner:

type Partial<T> = { [P in keyof T]?: T[P] }

In example, it means given I have type:

interface Person {
name: string
age: number
}

The Partial<Person> will be equivalent to following declaration:

interface Person {
name?: string
age?: number
}

Doing the same in Scala is not a one-liner, unfortunately.

It’s also simple to create new types by removing some properties, and so on. The rule of thumb is that if you find yourself repeating type definitions with minor updates, you’re doing it wrong and should rely on mapped types instead.

Collections

You will definitely miss Scala collections with functions like groupBy, filter, sort, partition and so on. You can import Underscore.js library that gives similar functionality.

For-comprehension

In Scala, we are used to using for-comprehension syntax when working with Options, Futures, Collections, Either, IO and so on. Unfortunately, TypeScript doesn’t have any conventions and so TypeScript devs are forced to reinvent the wheel in each case. I think this is a really good lesson on how failing to see an abstraction leads to complexity.

Let’s start with Options. The idiomatic way to define that something is optional in TypeScript is by using a union type: `let a: number | undefined`. Chaining multiple optional values is quite a common task and so we need a specific syntax — optional chaining:

let result = response?.data?.someField

Another common task is to use a getOrElse method, and so we have a special syntax for that as well — nullish coalescing.

let result = response?.data?.someField ?? ""defaultValue"

The situation is different for Promises. They are the syntax sugar for callbacks and if you want to chain 2 promises, you should await for the first one using the new syntax word — `await`.

let response1 = await myFunction1WithPromise()
let response2 = await myFunction2WithPromise(response1)
if (response.ok) … // code

Here, we are chaining multiple promises as we do a flatMap and map with Futures. Note that it’s quite easy to write sloppy code and forget to await the last promise if you want to return its result.

Let’s look at collections. There is no special syntax for map/flatMap, so you really need to use them directly.

let array = [1, 2, 3].flatMap(i => [i, i]).map(…) 

We don’t have any specific types and syntax for Either (such type isn’t even in the standard library). Note that it’s possible to rely on fp-ts lib, but I hadn’t tried that yet.

Summary

I had lot’s of fun working with TypeScript. It’s definitely worth it to be familiar with TypeScript because I don’t know other mainstream languages that give you structural subtyping with such scale.

Structural types are quite limited in Scala, and they will be improved in the next version, so it would broaden your view and shed a new light onto your Scala codebases as well. One interesting application of them is the ditching of database layers for DTOs. Some features are only coming in Scala 3, but you can try them in TypeScript — like singleton types for primitives, union, and intersection types.

Although TypeScript is a fun language to work with, I found a few things that to me seem to add too much verbosity — having to put return statements everywhere is really annoying, and the whole separation between statement and expressions in the programming language looks artificial after Scala. Also, some things will force you to use let instead of const (var and val equivalents in Scala).

Working within the JavaScript ecosystem can put you in a situation where you can lose strong typing, so you have to be extra careful. Getting bugs with `undefined` on production is really scary after Scala — it happened to me a couple of times. Usually it happens because type definitions are not correct.

Overall, I appreciate how TypeScript made working with the JavaScript ecosystem easy and safe. Yet, you should definitely keep an eye on some things and be prepared for more verbosity in day-by-day stuff if your background is in Scala.

--

--