Angela Oketch

She works with the Nation Media Group, where she is responsible for supporting innovation on the health desk. Her work has won her several accolades including the Annual Journalism Excellence Awards…

Smartphone

独家优惠奖金 100% 高达 1 BTC + 180 免费旋转




Improving the Developer Experience with TypeScript

So why TypeScript?

At 22:23 of Ryan’s talk, “10 Things I regret About Node.js”

Ryan described TypeScript as absolutely beautiful, a statement I now whole-heartedly agree with. He didn’t really elaborate much on why, but he got me real curious. I wasn’t the only one. In the days after the talk the downloads for type packages on npm skyrocketed.

Notice that this surge in interest isn’t temporary. People seem to stick to TypeScript after giving it a try

After seeing the talk I decided to give TypeScript a try. A few months later TypeScript is my starting point for each new project I start.

That quote comes from TypeScript’s own website. Notice the emphasis on tools. I believe the tooling around TypeScript is the biggest contributors it’s consistent growth and success. That tooling is very much owed to its very powerful static type system. Together, the tooling and type system work like a charm.

I want to make a case for why you, a JavaScript developer, might want to give TypeScript a try. To make that case I want to showcase some of the areas where I believe TypeScript really shines.

We’ve all seen this before, some regular old variables. A message that’s a string, a count of 50 and a person object that has two properties, firstName and lastName.

And due to the dynamic nature of Javascript, we can go ahead and do this.

This is obviously wrong. But these sorts of type errors are typically a bit more subtle, like forgetting to call parseInt on the value of a number input and trying to do some math with a string.

Let’s switch to TypeScript by changing the file extension from .js to .ts. If you try to hover over any of these variables, you will see that they have a certain type.

TypeScript infers the type of the variables from the initial values assigned to them. Since message is initialised as "Hello, world!", it’s easy to tell that it’s a string. Same with count and even with more complex object types like person.

Now when we attempt to do the same thing, we get these red squiggly lines indicating that something is wrong.

We’ve learned that TypeScript can infer types from the initial values of variables. That frees us from having to annotate the types of most variables. But what about cases where we don’t know the initial value of variables?

Why aren’t we getting an error here?

The count variable has a type of any. The any type is TypeScript’s way of saying “anything goes”. A string, number, object or array, any type can be assigned to any. When TypeScript doesn’t have any type annotations and lacks the context to infer the type it defaults to any.

We can fix this by annotating the type.

The part on the right of the colon : is the type of the variable. The basic types you can use are:

Then we have objects, arrays and functions. Their types are a bit more complex and much more powerful.

To begin, let’s show a really basic example of a function that takes two numbers and adds them together in regular Javascript.

To us humans it’s obvious that this function is supposed to add two numbers together, but there’s no reason for the computer to think that adding two strings is any less valid than two numbers. Since TypeScript has no information about the parameter types it defaults to any.

It’s verbose and really repetitive. JSDoc’s a great project and I’ve enjoyed using libraries that utilise JSDoc comments. But writing my own JSDoc code feels exhausting and tends to takes up an inordinate amount of vertical space.

Now let’s switch to TypeScript:

Like with variables, we can annotate the type of function parameters.

We can add two numbers like the function was intended to. But notice the red squiggly line when we try to add two strings together.

TypeScript lets us know that the types are incompatible. This is all well and good for these obvious cases but gets much more powerful once we get to objects and arrays.

*Interfaces have a broader definition which we’ll get into later.

These interfaces can be used as type annotations, just like strings and numbers.

The Person interface has two properties, firstName and lastName, both string. The object we assigned to john matches the Person type so TypeScript accepts that. But if we were to add a property that doesn’t exist on Person then we would get a type error.

TypeScript is really good with error messages. It lets us know that the age property doesn’t exist on Person so we can go right ahead and fix that.

But what if we wanted to add a middleName property to Person? Not everyone has a middle name.

Notice the question mark after middleName. It denotes an optional property.

And you can even have interfaces as properties of interfaces.

And once you have an interface, you can import and export it like any other class or function and reuse it throughout your project.

Oh, and I almost forgot my favorite part…

You can press Ctrl + Space at any time to get autocompletion!

Let’s take another look at the Company interface we saw earlier.

The employees property has a type of Person[], an array of Person. You could also define an array of Person as Array<Person>. The array literal syntax is more common, but the Array<T> syntax will play an important role later.

Here we have an example company with some employees. Say we want to calculate the average age of an employee in this company.

We get autocomplete for the Company’s properties, and with it we can see that employees is an array of Person.

So when we look at the autocompletion for bigCompany.employees we get all the Array.prototype methods, including map. One important detail to note is that map’s callbackfn has a value parameter of type Person.

TypeScript knows that since employees is of type Person[], the map method’s callback function will take in a Person.

Since each employee is of type Person, we get autocompletion for their properties and the types of those properties. We’re trying to calculate the average age of an employee, so let’s create an array of the employees’ age properties called employeeAges.

TypeScript knows that Person.age is of type number, so when the map’s callback function returns that property the map method’s return value will be of type number[], an array of numbers.

So when we sum the employeeAges with reduce, TypeScript knows that the values will be of type number.

I think you see where I’m going with this, TypeScript is smart.

What makes TypeScript so smart?

Let’s take a better look at the Array’s map method. We can find the type definition for it by right clicking on map and selecting “Go to Definition” in VSCode (or pressing F12).

Go to Definition (F12) and Go to Type Definition (Shift + F12) both do the trick

This brings us into a massive type definition file.

TypeScript has type definitions for all built-ins that exists in Javascript. Here’s the Array type definition with only the map method.

To make this even simpler, let’s only include the relevant parts for this explanation.

Array is a generic. Here’s a snippet from TypeScript’s documentation

Remember when I explained that Person[] and Array<Person> were equivalent? Person[] is a syntatic sugar for Array<Person>. The T in Array<T> is a type argument. Non-primitive types in TypeScript (e.g. functions and classes) may specify type arguments inside of a pair of brackets <> separated by commas (just like parameters).

Type arguments are commonly used for annotating the types of properties, methods and parameters. For the Array<T> the T represents the type of elements in the array. So if we create an array of type Array<Person>, every element in that array should be of type Person.

Look again at the map method definition.

The callback function’s value parameter is of type T, the type of the elements in the array. That explains how we get the correct type of the value parameter on the map method, but what about that U type argument?

U is used in three places:

In the case of Array<T>, the type we pass to <T> is used as the type of T in Array’s properties and methods. So the type of value in the map method, which is defined as the type of T, will be the type we assigned to T in Array<T>.

Following that logic, the type we pass to the U type argument in map<U> will be the type of U later in the map method.

And that is exactly what happens. If we set U as string the return type of the callback function will be string and the return type of map will be string[].

So if the callback function returns the incorrect type, TypeScript will give you a type error. If you hover over the error you would get the message:

But earlier we didn’t have to explicitly set the type of U. We could just use the map method and the return type of the callback function would be the type of elements in the resulting array. That’s because TypeScript understands context.

If the U type argument is not passed to the map method, TypeScript can infer it from the return type of callbackfn.

Here TypeScript knows that num is of type number. It knows that the return type of num.toString() is string. Therefore U must be a string, hence the return type of map, U[], must bestring[].

We will touch on generics and type arguments again later in this article. But before we do I want to explain another concept.

“Union Types” is just a fancy way to say “it’s one of these types”.

The syntax look like this:

The vertical bar (|) acts as a separator between the possible types of values this variable could hold at any time. This also prevents us from assigning the wrong values to this variable.

That’s cool and all, but what’s even cooler is that we can narrow union types down. Consider this example:

If we would try to assign a type of string | number to a variable of type string outside of the conditional, we could get this error:

We’re not getting an error though, so what’s going on?

When we’re doing the typeof comparison, we have no idea whether stringOrNumber is a string or a number.

But if the typeof stringOrNumber === "string" expression returns true, we can be confident that it’s a string. So when we check the type of stringOrNumber inside of the conditional we will see this.

So we’ve narrowed the type down to string and are able to assign it to another variable of type string. And if the expression returns false, we can be pretty sure it’s not of type string. That only leaves the number type left.

And voila, that’s union types. Let’s see a more practical example though

Imagine we’ve sent a POST request to /files to generate a new file. This request returns an object with a single field, fileId of type string

For some reason this request can take a few seconds to process, so the backend developer came up with a solution. You can send a GET request to/files/:fileId/status periodically to see whether or not the file is ready.

There are three possible states, waiting, ok and error. If the status is ok the response will look something like this.

If the file is not ready, we simply get a status of waiting.

But the file generation process could go wrong and respond with an error. If it does, the response would look something like this.

So we can define the response type to be something like this.

Let’s create an async function, createFile(). It should create the file, wait until it’s generated and ready to be used, and then return it.

We can start off by sending the POST request to /files. We’ll use the axios library for dealing with network requests.

AxiosPromise<T> is just a wrapper around a Promise which sets the type of the data field to the type of T (plus some other properties like headers).

To simplify, we can read AxiosPromise<T> as Promise<{ data: T }>. The T in Promise<T> is the type of the value that the promise resolves to.

If you’re comfortable with object destructuring, we can also write this as:

Okay, we started the file generation process, now we check whether the file is ready in 1 second intervals. We can implement the status checker as something like this:

Let’s start with the first step, getting the file status

As we talked about earlier, the FileStatusResponse‘s status is either ok, waiting or error. So let’s see it.

Okay yeah, the status property is of type "ok" | "waiting" | "error". But where are the file field for the "ok" status and the error field for the "error" status?

Intellisense makes me happy

They aren’t available because we haven’t narrowed it down enough yet. We have no idea whether the file or error fields exist or not. But if we narrow down the type, we can find out.

Since we know that the status is "ok", TypeScript knows that the file property is available, and that it has two fields, fileName and content, both of type string.

Since we’ve ruled out the “ok” status, TypeScript knows that the only possible values are “waiting” and “error”

You get the point. I won’t bore you with the rest of the implementation so we can move on to the next section.

Type arguments have a surprising degree of depth. In this section I’m going to cover two keywords I find myself using again and again, keyof and extends.

The keyof keyword creates a union type of every key an object has.

This getValueOf function takes in two parameters, an object and a key. What we want to ensure is that the key matches a key of the object we passed.

The extends keyword acts as a constraint when it comes to type arguments. It means that the type on the left hand side must be that type or a subtype of the type on the right hand side.

In other words, the type on the left must be as or more specific than the one on the right. Here’s an example:

What this says is that the type of the point must be as or more specific than Point2D. Given these constraints, these two statements are perfectly fine:

The z axis adds specificity. Given the presence of the x and y axises, both of the points above are specific enough to be used as Point2D.

This on the other hand is not specific enough, we need more information if we want to use this as a Point2D.

The same logic can be applied to union types.

Let’s see what happens when we use the getValueOf function with obj.

When the obj is passed as the first argument the definition of the obj parameter changes to { foo: string; bar: number; }. At the same time the key parameter changes to be a union type of the different keys present on obj.

Like we learned earlier, the left hand of extends must be as or more specific than the right hand side. So what’s more specific than "foo" | "bar"?

Both "foo" and "bar"!

As soon as we enter quotes, we get a dropdown with the different possible keys. And even cooler, check out the inferred return type of getValueOf.

The return type is T[K], meaning that the return type is the type of the property of T that K references.

These are topics I don’t intend to tackle in depth here but are useful to be aware of if you intend to dive deeper into TypeScript.

There are some ESLint rules that are not present in TSLint. I find that most of them are inconsequential but if you care about them tslint-eslint-rules offers a way to get them.

If you have never heard of ESLint or TSLint before, hit up Google for “eslint” or “tslint” and see what you find. Linters are really helpful tools.

Back in 2016 Eric Elliot argued that static types tend to give developers a false sense of security and provided some evidence that static types did not reduce bug density to a significant degree. He asserted that “Type correctness does not guarantee program correctness.” and pushed developers to focus instead on TDD (Test Driven Development).

And I agree with that notion. TypeScript has not rid me of all the bugs plaguing my programs, and the tool that should be used to combat those bugs should be better tests and better test coverage. Static types seem to correlate to a slight reduction in bugs, but they are not a silver bullet that will make your code bug free.

I also read an article which talks about types in Javascript with an angle on trust (or lack thereof) in other developers and external data. It argues that using TypeScript requires that you shift your trust in data being correct to other systems and developers.

That aside, the tools that TypeScript provides have enhanced my productivity and elevated my enjoyment of front end development significantly.

TypeScript has been a game changer for me. Before I tried it out I hadn’t really explored statically typed languages that much. I expected them to be a burden and really slow down development. My experience has been the complete opposite.

The autocompletion and intellisense are really helpful for remembering the types and order of parameters. The immediate error messages I get in my editor when I use the wrong type or have a logical error in my program allows me to fix my mistakes before I test or even run my code. And most importantly, reading my own and other people’s code is much easier. People often repeat this but it’s true, types are a great form of documentation. A variable called data really tells me nothing, but knowing the shape of it usually makes its purpose immediately clear.

This was in no way meant to be a comprehensive “Getting started” guide to get you up and running with TypeScript. I simply wanted to showcase some of the aspects and features that I love in TypeScript. If you found them as appealing as I do I highly recommend trying it out for yourself.

Add a comment

Related posts:

How to use animated icons on a website or a mobile app

Welcome to the second post in the #WeekOfIcons series. Week Of Icons is an initiative by Adobe and Iconfinder that celebrates the craftsmanship of icon design. This year, we focus on a fusion of icon…

What is Node.js used for?Applications of Node.js

Node.js is a scalable event-driven JavaScript environment. Explore more about Node.js & its applications in this post.

Higher Order Derivatives

First order derivative: f'(x) or dy/dx, also called "First Derivative". “Higher Order Derivatives” is published by Solomon Xie in Calculus Basics.