Fun with Types
Solving dynamic type problems with TypeScript
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…
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.
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
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: trueflag under
tsconfig.jsonfile in order for this example to work. The code samples are hosted on the TypeScript playground. You can check the
noImplicitThisoption if you click on the options button.
ThisType<T> is a built-in type that, as its name suggests, allows us to overwrite the
this type with 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
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
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
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
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.
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:
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
methods parts of a Vue component:
Component<C,D,M> receives three generic arguments;
Methods. These must fulfil the types that we have defined above;
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
methods properties types:
We then pass these types to the
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 (
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.
.remove() method, because I mistyped it as
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.
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.