loading...
Cover image for TypeScript Types Deep Dive - Part 1

TypeScript Types Deep Dive - Part 1

vintharas profile image Jaime πŸ”₯πŸ§™β€β™‚οΈπŸ”₯ Originally published at barbarianmeetscoding.com ・Updated on ・11 min read

This article was originally published on Barbarian Meets Coding.

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.

Haven't read so much about TypeScript yet?

This series of articles assumes that you have some knowledge of TypeScript. If you haven't dabbled so much into the world of TypeScript just yet, I recommend you to take a look at this introductory article.

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

Would you like to Experiment with TypeScript type system quickly?

A great way to experiment with TypeScript and its type system without even having to install TypeScript is to jump right into coding using the typescriptlang.org TypeScript playground. Jump in, type some code, and hover over the variables, functions and classes to find their 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

TypeScript will always try to do its best to make sense of your code. The TypeScript compiler has this mechanism called type inference by which it will try to interpret your code and find out the types of things so that you yourself don't need to type everything by hand.

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);
}

Really? Would I need to add all attribute names by hand?

The example above was meant to help you understand the usefulness of literal types and how to create type unions. However, it may have given you the false impression that you need to type in all the properties for an SVGElement by hand. Although you can do that, TypeScript has far more interesting features that can make working with types and extracting type information from existing types really convenient. In later articles in this series you'll learn about an alternative way to type the function about using generics and the keyof operator.

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:

The union | and intersection & operators are what we call type operators in TypeScript. Just like operators such as + or - allow you to perform operations on values, the type operators perform operations within the mystical universe of types.

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!


  1. I misspelled misspelling. ha. ha. ↩

Discussion

pic
Editor guide
Collapse
josema88 profile image
Jose OrdoΓ±ez

Great article, thanks Jaime! I'm just get into TypeScript and frontend stack. I define myself as a backend dev that loves Java and c# and I had some concerns about Javascript and it's maintanability at big projects, but TypeScript seems to be the one. Also the latest updates on ES contribute to Javascript to be a more robust language, I wonder if there will be a moment when TS and JS become one once for all...

Collapse
vintharas profile image
Jaime πŸ”₯πŸ§™β€β™‚οΈπŸ”₯ Author

TypeScript does feel very comfy to developers proficient in Java and C# :D. Who knows what shall happen 5 years from now... :D

Collapse
carlillo profile image
Carlos Caballero

Thanks Jaime!

Waiting for the next part! πŸ™ƒ

Collapse
vintharas profile image
Collapse
udondan profile image
Daniel Schroeder

I just started with Typescript last week. Very helpful article. Thanks!

Collapse
vintharas profile image
Jaime πŸ”₯πŸ§™β€β™‚οΈπŸ”₯ Author

Glad that you found it helpful! :D Let me know if I can help. I'm on the twitters @vintharas .