Typescript From a Scala Programmer's Perspective

Programmers (with Focal Point)

At Bright IT, we primarily use Scala for backend development. Scala is known, among other things, for its strong and expressive type system, which stands in sharp contrast to what Javascript is known for. As many of our developers write code for both frontend and backend, we'd prefer using a language closer to Scala to make transitions between the environments as easy as possible. Both Flow and Typescript are good options here - their goal is to add static typing to Javascript while remaining as close to it as possible.

This is important for two reasons - we do not want to abandon Javascript's enormous ecosystem of libraries and we want the transition to be as easy for our frontend-focused developers as possible. After a bit of research, we've decided to give TypeScript a try and we would like to share our experience using it.

One important thing to note is that all the examples in this article assume --strict flag, which turns on all stricter type checking behaviors.

The Good

TypeScript is Not a Separate Language

For the most part, writing TypeScript feels like writing JavaScript with types. Most patterns that work in JavaScript work and can be typed in TypeScript. This means that developers used to JavaScript can be converted to TypeScript without much trouble and can become productive right away. We'd like to highlight three reasons why this is the case. The first, and the most mundane one for Scala developers, is type inference. In TypeScript, it's possible to skip a type annotation, in which case the type checker will automatically deduce the type based on what is assigned to the variable. This alone means that obvious type annotations, such as let str: string = '', can be omitted. Despite this being a seemingly small thing, needing type annotations in every single place is not something many people are fond of, and with good reasons:

Second important TypeScript's feature is structural typing. Structural typing means that types with the same members are interchangeable - if a function needs object with fields name: string and age: number, it doesn't matter if the object is called Employee or Person:

This approach, which is sometimes called "static duck typing", matches how JavaScript programmers think about objects, which makes transitioning that much easier. Similarly, it's common practice in JavaScript to have special-case code at the beginning of functions if some argument is null. To support this, TypeScript has flow-sensitive typing - variable types can be adjusted based on conditions in ifsandwhile`s:

To sum up: TypeScript authors have taken care to ensure that most common JavaScript idioms can be easily translated into TypeScript and the results of that effort are clearly visible.

The Type System Is Remarkably Powerful

Despite simple code examples being simple to write, TypeScript's type system is very expressive, even when compared to Scala. It supports multiple features not (yet) present in Scala, such as literal types, union types and polymorphic functions. Describing these features is a topic on its own, so we'd like to highlight one library as an example of what they allow: io-tsio-ts helps with verifying at runtime that values conform to some type by defining validators for types. As the name suggests, its typical usecase is validating results of IO, such as HTTP requests. Here's how it works:

It's interesting to note that Scala libraries typically take the reverse approach: the validators are automatically generated with a macro from the type to validate. io-ts instead leverages the type system to calculate the type based on the validator.

The Bad

TypeScript Is Not a Separate Language

Again, it's a design goal of TypeScript to be a typed JavaScript. This means that some of its features are not as coherent as they'd be if all parts of the language were created with static typing in mind. As an example: unlike Scala traits, TypeScript interfaces do not support default method implementations, something which any Scala programmer will very quickly find out. In theory, already existing features can be leveraged to create mixins, which do allow default method implementation. However, the approach presented in TypeScript documentation (http://www.typescriptlang.org/docs/handbook/mixins.html) has multiple problems, such as type checking if an implementation is missing and requiring redeclaration of methods with default implementations. In addition, it's simply incompatible with --strict.

It's possible to adjust the examples from the documentation to work with --strict, as presented in a blog post here (https://blog.mariusschulz.com/2017/05/26/typescript-2-2-mixin-classes), but we discovered that this approach, in turn, prevented type declarations from working correctly (in particular: it hid fields added to Vue as interface extensions). A detailed comparison between both approaches and Scala traits would be a topic for a separate post, but it suffices to say that mixins are a second-class TypeScript feature, at best.

Untyped Code Requires Good Discipline

TypeScript supports a "dynamic" type called any, which essentially turns off type checking:

Its intended purpose is to help when migrating code from JavaScript or to allow using an untyped JavaScript library. There's a small problem with it, however. Variables of type any can be assigned to variables of all other types, and no runtime checks will be added when this happens. In practice, this leads to situations like the following:

Now, what happens if a developer notices this kind of error? They have no choice but to manually search every single place in the codebase where a value is assigned to name and manually check if it could have been infected with any. It's perfectly possible that the place where an invalid value is assigned could be five steps removed from actually assigning anything to .name. This is not something that a Scala programmer would expect from a type system. In particular, in Scala we could write similar code using manual type casts (.asInstanceOf), but JVM throws an exception immediately if we try to cast Integer to String.

The issue becomes more problematic with every additional source of any, like an untyped library. In our case, it was particularly painful as used TypeScript with Vue. Both Vue templates and Vuex (Vue Store) are completely untyped, which meant that more-or-less half the codebase was any-typed. As you might imagine, this was a constant source of any errors like the one above. In principle, any is not a bad idea, but it needs to be carefully monitored.

The Ugly

TypeScript is type-unsafe as a design goal. Scary wording aside, what does this actually mean? To shortly explain a nuanced matter, a language is type-safe when a variable of some type can only contain values of that type during program execution. Explicit "type holes" such as any or .asInstanceOf are not considered for type-safety, because circumventing the type system is their explicit purpose. However, in TypeScript, simple arrays are unsafe:

Using only arrays, we've managed to assign a Dog to a Cat. In the process, we've assigned Cat[] to Animal[] - the technical name for this behavior is covariance. Covariant arrays are a well-known issue and Scala avoids it:

You might be wondering - why didn't we define the meow/woof methods in Scala? Well, the reason is simple - if we didn't define them in TypeScript, Cat and Dog would be type unsafe by themselves:

Wait, what? Here's what happens: in TypeScript, all types are structural. Since neither Cat nor Dog have any members, they are compatible with one another. This is despite the fact that we can tell the difference:

The Verdict

So: is TypeScript worth it? The answer is: well, it depends. It's important to understand that TypeScript's explicit design goal is to statically verify as much already existing JavaScript as possible and to strike a balance between type safety and productivity; however, the language is obviously biased towards the latter. If your primary goal is to write frontend code with as much type safety as possible, you might want to look somewhere else. If you want to remain close to JavaScript, Flow might be just what you need.

Despite that, we'd describe our overall experience with TypeScript positively. TypeScript was easy to introduce to JavaScript developers. For the most part, we had no trouble finding typed libraries. It did help us find errors even during migration from plain JavaScript, and we're happy with the expressivity of the type system. We cannot honestly say that our experience using TypeScript with Vue was positive, but we're much more satisfied with the combination of TypeScript and React.

To conclude: if you're primarily looking for a way to make your frontend code safer without too much overhaul, we encourage you to give TypeScript a try!

Want to Talk More About This?

We look forward to meeting with you.

Get in touch - Klaus Unterkircher, Bright IT