Thinking in TypeScript

On the Office Online team, we regularly hire experienced engineers that have never worked on large scale web applications. As a result, we have a large number of engineers with strong backgrounds in languages like C# and Java learning how to write code in TypeScript for the first time. This transition comes naturally to most engineers, as some of the syntax may feel very familiar to C# and Java developers, but I think there is a lot to be gained from spending some time to learn how to leverage some of the unique capabilities of the language. This blog post will focus on some of the tips we’ve often found ourselves sharing in code reviews for developers that are new to TypeScript. I hope you find them helpful!

You might not need a class

Developers with a background in Object Oriented programming languages are used to encapsulating code within classes. An example we see regularly from new developers is the creation of utility classes that contains some static methods. Consider this example:

While this is a perfectly reasonable way to organize your code, you could instead create a module that exports a set of utility functions:

While it is nice that the utility function is less verbose and generates less JavaScript when transpiled to ES5, this isn’t the primary reason why we prefer to use functions over classes in many situations. By favoring functions over classes, developers will naturally be writing code that adheres to the principle of Composition over Inheritance, which we’ve found makes it easier for us to write components that are testable and loosely coupled.

An interface might not be what you expect it to be

A common source of confusion for developers new to TypeScript is the concept of interfaces. Developers coming from a C# or Java background are familiar with the concept of interfaces used in a context like this:

While this is perfectly valid TypeScript, we rarely ever write code that looks like this inside the Office Online codebase. If you read the TypeScript documentation for interfaces, you’ll see them described as a way to define the types of a JavaScript object rather than something that should be implemented by a class. This means we could actually write code that looks like this:

Now, this code is fundamentally different than the code in the previous example. The prior example created an instance of a class, which has a constructor and could also have methods, and assigned it to a person variable. The second example simply created a plain JavaScript object and assigned it to the person variable. When following the second approach, developers will typically use pure functions to manipulate these plain JavaScript objects rather than having methods to manipulate the state contained within the person object. This approach allows for a clean separation of data and the code that manipulates data, which is something I’ve found makes code easier to reason about and unit test.

You may have noticed that these first two guidelines have recommended that you avoid using classes. While I personally prefer plain JavaScript objects manipulated by pure functions over classes with methods, this does not mean I think you shouldn’t use classes at all in TypeScript. In fact, I actually encourage them to be used for things like stateful React components. These recommendations are mostly about demonstrating the new possibilities a developer has for structuring their code when introduced to a language that has first-class functions.

The types don’t exist at run time

The types provided by TypeScript allow some bugs to be identified through the static analysis performed by the compiler. TypeScript is ultimately transpiled into JavaScript that is executed by the browser. Since JavaScript does not have a type system as strong as the one provided by TypeScript, the type information from TypeScript code will be stripped from a program as a result of the transpilation process. The end result is that you do not have access to the types of an object at run time like you do in some other languages like C#.

While the inability to access type information at run time may seem like a deal-breaking constraint, TypeScript supports Discriminated Unions as a mechanism for differentiating objects of different types at run time. To better understand how discriminated unions work, let’s consider an application that displays a number to the user and supports a set of actions that manipulates that number.

In this example, we have a function getNextState that takes in an Action and the current state and returns the next state. Action is a union type of the Add and Decrement interfaces. Both of these interfaces have a type property, referred to as the discriminant, that allows us to determine which Action type was passed in to the getNextState function.The TypeScript compiler is able to leverage this type property within each clause of the switch statement to narrow down the set of valid properties to just those that correspond to the interface with that type value. For example, the compiler knows that the action inside the ‘Add’ case has a numberToAdd property and it knows that any reference to numberToAdd inside of the ‘Decrement’ block is a compiler error. The use of a discriminant allows us to differentiate objects of different types at runtime while also providing us with stronger type checking and an improved intellisense experience. When discriminated unions are combined with TypeScript strict mode, the compiler is able to do exhaustiveness checking on the switch statement inside the getNextState function, which is helpful to ensure that developers update that function as new Action types are added.

Use undefined instead of null

As Douglas Crockford has said many times, one of the annoying things in JavaScript is that it has two bottom values: null and undefined. Developers coming from other programming languages are probably familiar with the null keyword and are likely to favor the usage of the null keyword over undefined. While there is nothing wrong with this approach, there are often scenarios where a variable might have a value of undefined, such as optional parameters, for a function. This would result in variables that can have a real value, null, or undefined, which means developers need to remember to check for both null and undefined. Of course you can get around this by replacing instances of

with

Since both null and undefined are falsy values. However, this approach won’t work if the type of foo also had valid falsy values (such as 0 for a number or “” for a string) that you don’t want to handle in the same fashion as null and undefined. As a result, we found it preferable on the Office Online team to avoid using null in our codebase, instead encouraging the use of undefined (almost) everywhere.

Conclusions

TypeScript has emerged as a powerful tool for writing large scale web applications. The static type checking provides familiarity to experienced programmers coming from languages like C# and Java without reducing the ability for your programs to leverage the large library of open source JavaScript libraries. Experienced developers should find the syntax of TypeScript to be very familiar, but I hope these tips help these developers reach out of their comfort zones to fully leverage the capabilities of the language.