Barbarian Meets Coding
barbarianmeetscoding

WebDev, UX & a Pinch of Fantasy

13 minutes readtypescript

TypeScript Types Deep Dive

The TypeScript logo followed by the subtitle Types Deep Dive
An epic journey of discovery into the mysterious world of TypeScript's type system.

TypeScript is a modern and safer version of JavaScript that has taken the web development world by storm. It is a superset of JavaScript that adds in some additional features, syntactic sugar and static type analysis aimed at making you more productive and able to scale your JavaScript projects.

TypeScript was first launched in 2012, and at the time it did bring a lot of new features to JavaScript. Features that wouldn’t be available in JavaScript until much later with ES2015 and beyond. Today however, the gap in features between TypeScript and JavaScript is closing, and what remains as TypeScript strongest value proposition is its amazing type system and the dev tools around it. This type system is the one that delivers on the promise of TypeScript: JavaScript that scales and what brings you a great develop experience with:

  • Instant feedback whenever you do something dumb
  • Powerful statement completion
  • Seamless semantic code navigation
  • Smart refactorings and automatic code fixes
  • And more

In this series of articles we’ll explore TypeScript’s comprehensive type system and learn how you can take advantage of it to build very robust and maintainable web apps.

Type Annotations

Type annotations are the core of TypeScript’s type system. They are extra tidbits of information that you provide when you write your code so that TypeScript can get a better understanding of it and provide you with a better developer experience.

Let’s say you have a function to add two numbers:

const add = (a, b) => a + b;

Only TypeScript has no idea that neither a nor b are supposed to be numbers. So we can be slightly more expressive and annotate these params with a type annotation:

const add = (a: number, b: number) => a + b;

Now TypeScript knows for a fact, that both a and b can only be numbers. So that if we, for some reason, decide to write the following bit of code:

add(1, 'banana');

The TypeScript compiler, our faithful companion, will look at our code and go bananas (it expected numbers and we gave it a fruit, how naughty).

What’s the best part about that? The best part is that we get this error immediately. Not within hours, or days or weeks when this code gets exercised in some production system by an unwary user. Nope! We’ll get this error within milliseconds of having introduced it. Great stuff. Short feedback loops. They make everything better. Like bacon, or… bacon.

Basic Types

The basic types in TypeScript correspond to the primitive types of JavaScript:

number
boolean
string
Date
Array<T>
Object

So that, if you want to define a string in TypeScript you’d type the following:

let myName: string = "Jaime";

Because TypeScript’s goal is to make your life easy, in situations like this it’ll be smart enough to infer the type of the myName variable so that you don’t need to explicitly annotate it. Which means that this is enough:

let myName = "Jaime";    // Type string

And so…

let myName = "Jaime";    // Type string
let myAge = 23;          // Yeah sure! Type number

And:

let myName = "Jaime";    // Type string
let myAge = 23;          // Yeah sure! Type number
let isHandsome = true;   // Type boolean
let birth = new Date();  // Type Date

Let vs Const

So if:

let myName = "Jaime";    // Type string

What is the type of the myName variable below?

const myName = "Jaime";    // Type ?

is it string? Is it const string? STRING? Is it a something else?

If you are like me, and you’ve never considered this conumdrum you may be as suprised (as I was) to find out that the type is "Jaime" (waaaaat?!?):

const myName = "Jaime";    // Type "Jaime"

If we expand the example to other primitive types we’ll see that:

const myName = "Jaime";    // Type "Jaime"
const myAge = 23;          // Type 23
const isHandsome = true;   // Type true
const birth = new Date();  // Type Date

What’s going on here? const in JavaScript and TypeScript means that these variables above can only be bound once as they are declared. Therefore, TypeScript can make the assumption that these variables will never change and constraint their types as much as it can. In the example above, that means that the type of the constant myName will be the literal type "Jaime", the type of myAge will be 23 and so forth.

And what about the Date? Why doesn’t const affect its type at all? The reason for that is that, since Dates can be changed any time, TypeScript cannot constraint their type further. That date may be now, right now, but someone could go and change it to yesterday any time tomorrow. Oh my.

Let’s take a closer look at literal types, what they are and why they are useful.

Literals Types

So:

const myName = "Jaime";    // Type "Jaime"

The type of the string above is "Jaime" itself. What does that mean? It means that the only valid value for the myName variable is the string "Jaime" and no other. These are what we call literal types and you can use them as any other type annotations in TypeScript:

const myName : "Jaime" = "Jaime";

So that if I try to be super smart and write the following:

const myName : "Jaime" = "John";

TypeScript will righteously step in with a compiler error:

const myName : "Jaime" = "John";
// => 💥 Type '"John" is not assignable to type '"Jaime"'

Awesome! So How is this useful? We’ll see in just a sec. But in order to give you a really nice example I first need to teach you another cool feature in TypeScript’s type arsenal: unions.

Unions

Imagine we are building a library that lets you create beautiful visualizations using SVG. In order to set the properties on an SVG element it’d be helpful to have a function that could look something like this:

function attr(element, attribute, value) {}

The type of each one of these attributes could be expressed as follows:

function attr(element: SVGCircleElement, 
              attribute: string, 
              value: string) {}

And you could use this function like so:

attr(myCircle, "cx", 10);
attr(myCircle, "cy", 10);
attr(myCircle, "r", 5);

This works but… What if you misspell an attribute?

attr(myCircle, "cx", 10);
attr(myCircle, "cy", 10);
attr(myCircle, "radius", 5); 
// => 💥 Doesn't work! There's no radius in SVGCircleElement

It blows up sometime at runtime. And although it may not explode outright, it won’t work as you expected it to. But isn’t this exactly what a type system and TypeScript should help you with? Exactly! A better approach is to take advantage of TypeScript type system and use type literals to further constraint the number of possible attributes:

function attr(element: SVGCircleElement,
              attribute: "cx" | "cy" | "r",
              value: string) {}

The "cx" | "cy" | "r" is a union type and represents a value that can either be of type "cx", "cy" or "r". You build union types using the | union type operator.

Excellent! So if we now make the same mistake than we just made a second ago, TypeScript will come to the rescue and give us some feedback instantaneously:

attr(myCircle, "cx", 10);
attr(myCircle, "cy", 10);
attr(myCircle, "radius", 5); 
// => 💥 Type '"radius"' not assignable to type "cx" | "cy" | "r"
// 🤔 Oh wait! So the radius attribute in a circle is actually called "r"!

By taking advantage of type literals you can constraint the available types to only the ones that make sense and create a more robust and maintainable application. As soon as we make a mistake like the one above, TypeScript will tell us and we’ll be able to fix it right then and there. Not only that, by making this rich type information available to TypeScript, the TypeScript compiler will be able to offer us more advanced features like statement completion and give us suggestions for suitable attributes as we type in our editor.

If you’ve done SVG visualizations in the past, the function above may look familiar. That’s because it is heavily inspired by d3.Selection.attr function:

d3.select("svg")
  .attr("width", 100)
  .attr("height", 200)

In a past project we run into several of these issues and we ended up creating boilerplate around d3 to avoid misspellings. After migrating to TypeScript we never had the same issue. We could rely on the expressiveness of type system to take care of that on its own.

// A possible (naive) type definition for d3Selection
interface d3Selection {
  attr(attribute: 'width' | 'height' | etc..., value: number);
}

Type Aliases

An attribute type defined as we did earlier can be confusing and cumbersome to reuse:

function attr(element: SVGCircleElement,
              attribute: "cx" | "cy" | "r",
              value: string) {}

Type aliases are a convenient shorthand to describe a type, something like a nickname that can be used to provide a more descriptive name for a type and allow you to reuse it around your codebase.

So if we wanted to create a type that could represent all the available attributes in an SVGElement a way to go about that would be to create an alias like so:

type Attribute = "cx" | "cy" | "r" // etc...

Once defined we can rewrite attr function signature:

function attr(element: SVGCircleElement,
              attribute: Attribute,
              value: string) {}

Arrays, Tuples and Objects

You can type an array in TypeScript by using the following notation:

let numbers: number[] = [1, 2, 3];

Or alternatively:

let numbers: Array<number> = [1, 2, 3];

I like the former because it involves less typing. Since we’re just initializing a variable TypeScript can infer the type, so in this case you can remove the type annotation:

// TypeScript can infer that the type 
// of numbers is number[]
let numbers = [1, 2, 3];

numbers.push('wat');
// 💥 Argument of type '"wat"' is not assignable to parameter of type 'number'.
numbers.push(4);
// ✅ Yes!
numbers.psuh(5);
// 💥 Property 'psuh' does not exist on type 'number[]'.(2339)

TypeScript also has great support for tuples which can be seen as finite arrays of two, three (triplet), four (quadruplet), or more elements. They come in handy when you need to model a number of finite items that have some relationship between them.

We can define a tuple of two elements like this:

let position: [number, number] = [0, 0];

If we now try to access an element outside of the boundaries of the tuplet TypeScript will come and save us:

let something = position[2];
// 💥 Tuple type '[number, number]' of length '2' has no element at index '2'.

We can follow a similar approach to define tuples with more elements:

let triplet: [number, number, number];
let quadruplet: [number, number, number, number];
let quintuplet: [number, number, number, number, number];
// etc...

On occasion you’ll find yourself using objects in TypeScript. This is how you type an object literal:

const position: {x:number, y:number} = {x: 0, y: 0};

Again, under these circumstances TypeScript can infer the type of the object literal so the type annotation can be omitted:

const position = {x: 0, y: 0};

If you are daring enough to try an access a property that isn’t defined in the object’s type, TypeScript will get angry at you:

const position = {x: 0, y: 0};

console.log(position.cucumber);
// 💥 Property cucumber doesn't exist in type {x:number, y:number}

Which is to say that TypeScript gives you MAXIMUM MISPELLING1 PROTECTION.

And just like we used type aliases earlier to have a more descriptive and less wordy way to refer to an HTML attribute, we can follow the same approach for object types:

type Position2D = {x: number, y: number};
const position: Position2D = {x: 0, y: 0};

Which also results in a somewhat more specific error message:

console.log(position.cucumber);
// 💥 Property cucumber doesn't exist in type Position2D

Intersections

Where the | union operator behaves like an OR for types, the & intersection operator behaves like an AND.

Say you have a type that defines a dog, which is something that has the ability to bark:

type Dog = {bark():void};

And another type that describes something which can be drawn:

type CanBeDrawn = {brush:Brush, paint():void}; 

We can merge both concepts into a new type that describes a dog which can be drawn using the & operator:

type DrawableDog = Dog & CanBeDrawn;

How are intersection types useful? They allow us to model mixins and traits with types in TypeScript, both patterns that are common in JavaScript applications. A mixin is a reusable bit of behavior that can be applied ad hoc to existing objects and classes, and extends them with new functionality. The & operator lets you create new types that are the result of combining two or more other types, just like mixins in JavaScript. If you aren’t super familiar with mixins I wrote a bunch about their strengths and weaknesses:

Wrapping Up

TypeScript’s expressive type system is, without the shadow of a doubt, the most interesting feature in the language and what makes it deliver on its promise of writing JavaScript that scales.

Using type annotations, you can provide additional type information to the TypeScript compiler so that in turn it can make your life as a developer easier, helping you build more robust and maintainable applications. Following that same philosophy, the TypeScript compiler will do its best to infer the types from your code without you having to explicitly annotate every single part of it.

The type annotations at your disposal are many and varied, from primitive types like number, string, to arrays, arbitrary objects, tuples, interfaces, classes, literal types and more. You can even define type aliases to provide descriptive names that make types easier to understand and reuse.

A particularly interesting set of types are type literals. Type literals represent a single value as a type. They are very useful because they allow you to constraint very finely the type of a variable or API. We saw an example of how you can take advantage of literal types to provide a safer API for the d3 visualization library.

Using type operators like union | or intersection & you can transform types into other types. This expressiveness and malleability of the type system allows you to model highly dynamic object oriented design patterns like mixins.

And that was all for today! Hope you have enjoyed this article which will be soon be followed by more TypeScript type goodness. Have a wonderful day!

TypeScript Types Deep Dive the talks

If you want to reinforce what you’ve just learned and like watching programming videos then you might enjoy these YouTube series:


  1. I misspelled misspelling. ha. ha.

Jaime González García

Written by Jaime González García , dad, husband, software engineer, ux designer, amateur pixel artist, tinkerer and master of the arcane arts. You can also find him on Twitter jabbering about random stuff.Jaime González García