Fun with Types

Solving dynamic type problems with TypeScript

Javier Iglesias García
Empathy.co
9 min readMay 2, 2019

--

Sheldon from The Big Bang Theory teaches you TypeScript

Over the past few months, TypeScript has become an increasingly popular language. According to the 2018 State of JS survey, if we omit the ES6 standard, it is the top trending JavaScript dialect. 80% of the polled developers shared an interest in TypeScript, whereas only 10% communicated a dislike for it.

If you look through the list of cool features that TypeScript offers, (writing es-next syntax today, customizing the build output, project references, typing pure JavaScript files, etc.), the survey results are hardly surprising. However, we shouldn’t let these features distract us from TypeScript’s main objective: To provide a dynamic type system that helps prevent errors at compile time. If we look at the contributions of the community to the DefinitelyTyped repository, TypeScript is doing a great job of achieving that objective.

It does all of this while being a JavaScript superset. What’s great about that? All of your current JavaScript code is already valid TypeScript code.

This programming language is not limited to primitive data types. It adds a whole new level of syntax for defining truly dynamic types. Interfaces, mapped types, conditional types, function overloading, module augmentation, union types, intersection types, these are just some of the things that this type system allows you to do. You can read about them all in their awesome documentation.

If you come from a static type language background, as I do, there are many features of the TypeScript type system that will never cease to impress.

But that’s quite enough TypeScript trumpet blowing for today. I want to show you a concrete example of how TypeScript provides us with the tools to solve a typing problem…

The problem:

At Empathy, we use the Vue framework to build rich user interfaces. This framework allows you to write Web visual components. These can range from a simple text-only paragraph with no behaviours, to something more complex, such as drop-downs, accordions, and even full pages packed with animations and interactions. The possibilities presented by Vue, and its ease of use, are fantastic. If you haven’t heard about it before, I encourage you to go and give it a try.

Each Vue component is defined by a simple JavaScript object. Within this you can write your methods, props (AKA inputs if you come from a framework like Angular), local component data, computed properties, lifecycle hooks… and so on. Inside this code, you can also use some utility properties or methods provided by the component instance ($slots, $scopedSlots, $props, $attrs, etc.).

Vue combines all of this information in order to compose the context of the component. It flattens the definitions, enabling you to access all of them from the this context. For example, if you have to access a prop inside a method, you don’t have to write this.props.myProp. You just have to write this.myProp.

For simplicity’s sake, we are only going consider computed properties, data, and methods. I don’t intend to explain how a Vue component works, nor how to type it completely. I want to illustrate how to solve a dynamic types problem using TypeScript and provide an explanation as to why I think it can help you develop reliable JavaScript applications.

With the above in mind, imagine we have a component definition like this*:

As you can see, there are several places where we access the this property, but right now we don’t have any type safe definitions. It’s possible that instead of typing this.collapsed = true, we could accidentally make a typo and write something like this.colapsed = true. It’s likely that we won’t notice that we mistyped ‘collapsed’ until the code is executed.

How can we write a type that makes the TypeScript compiler know that ‘this’ is the intersection of data() + methods + computed? There are a couple of things that will be able to help us; ThisType<T> and type inference in conditional types. Before I outline a solution, let’s take a quick look at both of these features.

* You will need to set the noImplicitThis: true flag under compilerOptions in your tsconfig.json file in order for this example to work. The code samples are hosted on the TypeScript playground. You can check the noImplicitThis option if you click on the options button.

ThisType<T>

ThisType<T> is a built-in type that, as its name suggests, allows us to overwrite the this type with the T type. If you are a veteran JavaScript developer, you may have used the Function.prototype.bind() function at some point in your life. This function, as MDN says, allows us to point the this keyword to another object; a typing problem that ThisType<T> sets out to solve.

How do you use it? The answer is pretty easy. With an intersection type:

Essentially, we are telling the TypeScript compiler to replace the this type with the Bar type. Of course, this is just a simple example. At some point in your code you will have to run obj.method.bind(someBarTypeVariable) before being able to call the function. You can play with the sample here and see the errors that the TypeScript compiler throws when trying to access non-existent properties.

Remember to check the noImplicitThis flag in the TypeScript playground.

Type inference in conditional types

Imagine that you wanted to write a function called callFn. It would receive a function as a parameter named fn, and would simply return the result of calling it. It would look something like this:

Since TypeScript 2.8, we’ve had access to type inference in conditional types. The fastest way to use this feature to type this callFn function is by using the ReturnType<T> util type. This type receives a function type as the T generic parameter and retrieves its return type.

Using it is quite simple. We just have to define our callFn function as a function with a generic parameter. Let’s call it FunctionType. Then, we set the return type to ReturnType<FunctionType>:

You may notice the extends keyword inside the generic type. When we use an extends X clause in a generic type definition, it is just a way of saying to the compiler that the generic parameter should fulfil this X type. In our case, it means that FunctionType should be a function with no parameters or result type. I have left an example here, so you can test it with any type.

Something else to highlight in this code is that, when you call the callFn function, you don’t have to explicitly write the generic parameter type. TypeScript is clever enough to infer it from the fn parameter.

ReturnType<T> internals

The ReturnType<T> type’s definition is a little complex, but I’ll try my best to explain what’s going on.

ReturnType<T> is defined like this in the TypeScript util types:

Here we are asking the TypeScript service whether or not the generic T type is a function. We aren’t concerned about parameters here, so have written ...args:any[].

In the event that T is a function, we are using the infer R code to tell the types service to save the return type to a new variable called R. If T’s a function, it will return the R type. If it’s not, it will return any.

This infer keyword only works within conditional types. That’s why this feature is called type inference in conditional types.

With the basics of both ReturnType<T> and type inference in conditional types explained. Let’s take a look at how we can use them to solve a dynamic type problem in a Vue component.

The solution

First of all, let’s define the type for each part of our Vue component:

Pretty easy, isn’t it? These aliases will help make our component type more readable.

Now, we are going to define a util type to extract the return types from a dictionary of functions:

This ReturnTypes<T> type receives a dictionary of functions as a generic parameter. Even though we are only going to use this type with component computed properties, which have no parameters, we have allowed these functions to have any parameters. In doing so, we ensure that this is a proper util type, and can be reused in other places of our code.

We then build an object with the same keys as this dictionary of functions. Instead of setting the function as the value of these keys, we set the return type of the function.

Finally, we write the component’s type. As I mentioned before, in order to simplify this example, we are just focusing on the data, computed, and methods parts of a Vue component:

The Component<C,D,M> receives three generic arguments; Computed, Data, and Methods. These must fulfil the types that we have defined above; ComponentComputed, ComponentData, and ComponentMethods. We need to link these three fields with their types and add the proper type for the this context; the union of the return type of each computed property, the return type of the data method, and the methods.

Now that it’s built, there are two options for how you can use it. You could either define those generic types in advance, or use a helper function so that TypeScript can infer the parameters. There is an open pull request that allows generic type inference to happen while also defining variables. It’s likely we will see that feature in a future release. For now, let’s check out both of the options that are available to you today.

Defining types in advance

Following the first option, we begin simply by defining our component’s computed, data, and methods properties types:

We then pass these types to the Component<C,D,M> interface:

Voilà! Since types are defined inside the interfaces, we don’t have to explicitly set types inside the component object, and we have full type checking inside the component object.

I’ve posted this solution’s full code here: on the TypeScript Playground.

Inferring generic types with a function

In this second option, our first step is to define the helper function:

As you can see, it is just a generic function with the same generic types we have been working with throughout this post. It has no logic, we are just going to use it to infer the types of the component definition.

The above is a dirty trick that TypeScript developers sometimes use to infer generic types. In my personal opinion, I wouldn’t recommend using it. You are modifying the behaviour of your code just to have proper typing. If this function were more like the Vue.component one (which has some logic), I’d be more inclined to use it.

If you do decide to use this function, you can then define your components like this:

I’d recommend that you write the return types for each function explicitly. If you don’t, TypeScript will try to read the code and infer the types. As each of the types for our component (computed, data, methods) can depend on the definitions of the others, as soon as you write a circular dependency in your code, TypeScript will fail inferring those types and will return any. Once again, this code is available on the TypeScript playground.

Conclusions

When I started coding with JavaScript, one of the primary issues I felt it had was the lack of types. Coming from a language like Java or C#, static type checking was really important for me. I remember always failing to call the jQuery .remove() method, because I mistyped it as delete(), or totally forgetting if a method was expecting a DOM node, a jQuery node, or a string selector. Things like that pulled me back from learning JavaScript for a while.

Don’t get me wrong. I love JavaScript. I fully believe that it’s a powerful language. It provides developers with enough flexibility and syntactic sugar to build complex systems while also being able to express their intentions clearly ,and cleanly, in the code. In fact, I’d go as far to say that it is my favourite programming language. The only problem that I have with it is that I miss a proper types system.

Fortunately, there are many developers in this awesome language community who are interested in solving this problem. This is what made the existence of TypeScript possible.

For me, TypeScript allows you to add types to your JavaScript code in a really easy and dynamic way. Its definition as a superset of JavaScript already implies that if you know JavaScript, you already know TypeScript. For anyone familiar with JavaScript, it shouldn’t be too hard to start using it in your projects. There are lots of features to get your head around, but its documentation, open source spirit, and awesome community will help set you on the right path.

Bonus

If you are interested in Vue and how a component is defined, you can check its definition file. You’ll see how they have used advanced TypeScript features, such as function overloading, to type the Vue.component method, which is used to globally register components in the application scope.

--

--