One important thing to note is that all the examples in this article assume
--strict flag, which turns on all stricter type checking behaviors.
TypeScript is Not a Separate Language
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
null. To support this, TypeScript has flow-sensitive typing - variable types can be adjusted based on conditions in
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-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.
TypeScript Is Not a Separate Language
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
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:
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
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.
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
.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
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
woof methods in Scala? Well, the reason is simple - if we didn't define them in TypeScript,
Dog would be type unsafe by themselves:
Wait, what? Here's what happens: in TypeScript, all types are structural. Since neither
Dog have any members, they are compatible with one another. This is despite the fact that we can tell the difference:
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!