DEV Community

John Au-Yeung
John Au-Yeung

Posted on

Type Inference in TypeScript

Subscribe to my email list now at http://jauyeung.net/subscribe/

Follow me on Twitter at https://twitter.com/AuMayeung

Many more articles at https://medium.com/@hohanga

Since TypeScript entities have data types associated with them, the TypeScript compiler can guess the type of the data based on the type or value that is assigned to a variable. The automatic association of the type to a variable, function parameter, functions, etc. according to the value of it is called type inference.

Basic Type Inference

TypeScript can infer the data type of a variable as we assign values to them. For example, if we assign a value to a number, then it automatically knows that the value is a number without us telling it explicitly in the code that the variable has the data type number. Likewise, this is true for other primitive types like strings, booleans, Symbols, etc. For example, if we have:

let x = 1;

Then the TypeScript compiler automatically knows that x is a number. It can deal with this kind of straightforward type inference without much trouble.

However, when we assign data that consists of multiple data types, then the TypeScript compiler will have to work harder to identify the type of the variable that we assigned values to it. For example, if we have the following code:

let x = [0, 'a', null];

Then it has to consider the data type of each entry of the array and come up with a data type that matches everything. It considers the candidate types of each array element and then combines them to create a data type for the variable x. In the example above, we have the first array element being a number, then the second one being a string, and the third one being the null type. Since they have nothing in common, the type has to be a union of all the types of the array elements, which are number, string, and null. Wo when we check the type in a text editor that supports TypeScript, we get:

(string | number | null)[]

Since we get 3 different types for the array elements. It only makes sense for it to be a union type of all 3 types. Also, TypeScript can infer that we assigned an array to it, hence we have the [].

When there’s something in common between the types, then TypeScript will try to find the best common type between everything if we have a collection of entities like in an array. However, it isn’t very smart. For example, if we have the following code:

class Animal {  
  name: string = '';  
}

class Bird extends Animal{}

class Cat extends Animal{}

class Chicken extends Animal{}

let x = [new Bird(), new Cat(), new Chicken()];

Then it will infer that x has the type (Bird | Cat | Chicken)[]. It doesn’t recognize that each class has an Animal super-class. This means that we have to specify explicitly what the type is like we do in the code below:

class Animal {  
  name: string = '';  
}

class Bird extends Animal{}

class Cat extends Animal{}

class Chicken extends Animal{}

let x: Animal[] = [new Bird(), new Cat(), new Chicken()];

With the code above, we directed the TypeScript compiler to infer the type of x as Animal[], which is correct since Animal is the super-class of all the other classes defined in the code above.

Contextual Typing

Sometimes, TypeScript is smart enough to infer the type of a parameter of a function if we define functions without specifying the type of the parameter explicitly. It can infer the type of the variable since a value is set in a certain location. For example, if we have:

interface F {  
  (value: number | string | boolean | null | undefined): number;  
}

const fn: F = (value) => {  
  if (typeof value === 'undefined' || value === null) {  
    return 0;  
  }  
  return Number(value);  
}

Then we can see that TypeScript can get the data type of the value parameter automatically since we specified that the value parameter can take on the number, string, boolean, null, or undefined types. We can see that if we pass in anything with the types listed in the F interface, then they’ll be accepted by TypeScript. For example, if we pass in 1 into the fn function we have above, then the TypeScript compiler would accept the code. However, if we pass in an object to it as we do below:

fn({});

Then we get the error from the TypeScript compiler:

Argument of type '{}' is not assignable to parameter of type 'string | number | boolean | null | undefined'.Type '{}' is not assignable to type 'true'.(2345)

As we can see, the TypeScript compiler can check the type of the parameter by just looking at the position of it and then check against the function signature that’s defined in the interface to see if the type if actually valid. We didn’t have to explicitly set the type of the parameter for TypeScript to check the data type. This saves us a lot of work since we can just use the interface for all functions with the same signature. This saves lots of headaches since we don’t have to repeatedly set types for parameters and also type checking is automatically done as long as define the types properly on the interfaces that we defined.

One good feature that TypeScript brings in the checking of data types to see if we have any values that have unexpected data types or content. TypeScript can infer types based on what we assign to variables for basic data like primitive values. If we assign something more complex to a variable, then it often is not smart enough to infer the type of the variable that we assigned values to automatically. In this case, we have to annotate the type of the variable directly.

It can also do contextual typing where the type of the variable is inferred by its position in the code. For example, it can infer the data type of function parameters by the position it is in the function signature if we define the function signature in the interface that we used to type the function variable that we assign to.

Top comments (0)