For many who came to programming via JavaScript it is easy to fall in love with its low barrier to entry and versatile nature. JavaScript runs in a browser, can be written in notepad, is interpreted line by line and requires no complicated compilation or tooling. JavaScript has democratized software development by allowing developers from all backgrounds to pick it up and start coding. But with Javascript’s forgiving nature, comes increased chances to make mistakes and create bugs.
Consider this JavaScript program:
function add(a, b) { return a + b; } console.log(add(1, 2));
This is a simple addition program that is designed to take two numbers and add them together, returning the result. Unexpected, often unpredictable things can start happening when we call this function with values that aren’t numbers.
We only really want to call this function with numbers - it doesn’t make sense otherwise - and in fact, if you pass values to it that are not numbers, you’ll end up with often bizarre and unpredictable results:
console.log(add(1, "2")); console.log(add(1, true)); console.log(add(1, "hello"));
JavaScript is a dynamically typed language. This means that the types of data that we store in our variables can change at runtime. This can make it complicated to capture our intention in our JavaScript code - that our add function should only be called with numbers.
What we are seeing happen here is called type coercion - JavaScript automatically converts values from one data type to another. This can happen explicitly, through the use of functions and operators, or implicitly, when JavaScript expects a certain type of value in a particular context. Implicit type coercion can sometimes lead to unexpected results, especially in complex expressions. Here are a few cases:
console.log(1 + "2"); console.log("2" + 1); console.log("5" - 1); console.log("5" * "2"); console.log(0 == false); console.log(0 === false);
Confusing right?!
In a JavaScript codebase, the best we can do is add some guard checks to our program that will throw errors if invalid values are provided. The problem with having only these runtime checks to protect us from errors is that we’ll often find out when a bug has been introduced into our code at the same time that our users do. So how do we protect ourselves from this often confusing JavaScript behavior?
TypeScript to the Rescue
TypeScript can help us to describe the intention of our code by being explicit about what types we expect where. TypeScript is a superset of JavaScript that adds additional type information to the language. When you compile TypeScript, JavaScript is produced that can run anywhere, so it’s really easy to incrementally use it to make your development experience better without having to rebuild all of your software.
Types allow you to catch errors in your code before it runs. If you accidentally assign a value of the wrong type to a variable, you’ll get a compile error. Types also make your code easier to read because you can explicitly state what kind of value you expect. We can also use tools to make types even more powerful. Code editors and IDEs have inbuilt tools to help autocomplete your code as you write when you’re using types correctly.
How do we add type annotations?
You add a type annotation in TypeScript by appending a colon (:
) followed by
the desired type after the function parameter’s name. If we were to extend our
previous add
example to convert it to TypeScript it would look like this:
function add(x: number, y: number) { return x + y; } console.log(add(1, 2));
This is the simplest change we can make to our code to make it more robust. Now
the compiler knows that any code that attempts to call add()
with anything but
two numbers will fail at runtime, so it will throw an error upon compilation to
tell you your program isn’t valid.
If we try and call add with a number and a string, for example, we’ll get a compiler error:
TypeScript is smart enough to work out some of the types in your program for you based on the information you’ve given it, we call this “type inference”. If we take the above example and expand it out, adding all the type annotations that we can, it’d look like this:
function add(x: number, y: number): number { return x + y; } const result: number = add(1, 1); console.log(result);
Here we’ve explicitly added annotations for the function parameters, a return
type of the add
function, and the variable result
. As a general rule,
developers add “just enough” type annotations to tell the TypeScript compiler
what’s going on, and let it infer the rest. In our original example, by adding
type annotations to the x
and y
parameters, the TypeScript compiler could
inspect the code and realize we only ever add two numbers, so would infer both
the function return type, and the type of the variable result to be of type
number
.
Even if you only ever use TypeScript to annotate function parameters you’ll immediately remove an entire category of errors from your code.
Turning your TypeScript back into JavaScript
If your project is built using Node, you will need to add the
typescript
package and run the
tsc
compiler tool. We’ve written an
introduction to configuring your TypeScript compiler.
Deno comes with the TypeScript compiler built right in, so if you’re using Deno, you do not need any other configuration or tools. Deno supports TypeScript out of the box, automatically turning TypeScript into JavaScript as we execute your source code.
On the client side, you can use Vite, a tool after our own heart, which does a similar transparent compilation for you, so if you’re a front end developer, you can still get TypeScript joy in your code.
Next up
In our next Deno Bite we’ll talk about common types that you’ll need in your TS code and how to use them to build more complex types to make your code clear and bug-free!
Are there any topics on TypeScript you would like us to cover? Let us know on Twitter or Discord!