Type inference with TypeScript using the as const construct and the infer keyword

TypeScript allows you to automate many tasks that, without using this language, developers have to solve independently. But when working with TypeScript, there is no need to constantly use type annotations. The fact is that the compiler does a great job of type inference based on the context of code execution. The article, the translation of which we publish today, is devoted to rather complicated cases of type inference in which the keyword inferand construction are used as const.



Type Inference Basics


First, take a look at the simplest type inference example.

let variable;

A variable that is declared in this way is of type any. We did not give the compiler any hints about how we will use it.

let variable = 'Hello!';

Here we declared a variable and immediately wrote a value into it. TypeScript can now guess that this variable is of type string, so now we have a perfectly acceptable typed variable.

A similar approach applies to functions:

function getRandomInteger(max: number) {
  return Math.floor(Math.random() * max);
}

In this code, we do not indicate that the function getRandomIntegerreturns a number. But the TypeScript compiler knows this very well.

Type inference in generics


The above concepts are related to universal types (generics). If you want to know more about generics, take a look at this and this materials.

When creating generic types, you can do a lot of useful things. Type inference makes working with universal types more convenient and simplifies it.

function getProperty<ObjectType, KeyType extends keyof ObjectType>(
  object: ObjectType, key: KeyType
) {
  return object[key];
}

When using the generic function above, we do not need to explicitly specify the types.

const dog = {
  name: 'Fluffy'
};
getProperty(dog, 'name');

This technique, among other things, is very useful in creating universal React-components. Here is the material about it.

Using the keyword infer


One of the most advanced TypeScript features that comes to mind when talking about type inference is the keyword infer.

Consider an example. Create the following function:

function call<ReturnType>(
  functionToCall: (...args: any[]) => ReturnType, ...args: any[]
): ReturnType {
  return functionToCall(...args);
}

Call, with the help of this function, another function, and write what it returns into a constant:

const randomNumber = call(getRandomInteger, 100);

The previous expression allows us to get what the function getRandomIntegerthat received the input returned as the upper bound of the random integer returned to it, 100. True, there is one small problem. It lies in the fact that nothing prevents us from ignoring the types of function arguments getRandomInteger.

const randomNumber = call(getRandomInteger, '100'); //   

Since TypeScript supports spread and rest parameters in higher order functions, we can solve this problem like this:

function call<ArgumentsType extends any[], ReturnType>(
  functionToCall: (...args: ArgumentsType) => ReturnType, ...args: ArgumentsType
): ReturnType {
  return functionToCall(...args);
}

Now we have pointed out that the function callcan process an array of arguments in any form, and also that the arguments must match the expectations of the function passed to it.

Now let's try again to make an incorrect function call:

const randomNumber = call(getRandomInteger, '100');

This results in an error message:

Argument of type β€˜β€100β€³β€˜ is not assignable to parameter of type β€˜number’.

In fact, following the above steps, we simply created a tuple. Tuples in TypeScript are fixed-length arrays whose value types are known but are not required to be the same.

type Option = [string, boolean];
const option: Option = ['lowercase', true];

Keyword Features infer


Now let's imagine that our goal is not to obtain what the function returns, but only to obtain information about the type of data returned to it.

type FunctionReturnType<FunctionType extends (...args: any) => ?> = ?;

The above type is not yet ready for use. We need to resolve the issue of how to determine the return value. Here you can describe everything manually, but this goes against our goal.

type FunctionReturnType<ReturnType, FunctionType extends (...args: any) => ReturnType> = ReturnType;
FunctionReturnType<number, typeof getRandomInteger>;

Instead of doing this on our own, we can ask TypeScript to output the return type. The keyword infercan only be used in conditional types. That is why our code can sometimes be somewhat untidy.

type FunctionReturnType<FunctionType extends (args: any) => any> = FunctionType extends (...args: any) => infer ReturnType ? ReturnType : any;

Here is what happens in this code:

  • It says FunctionTypeexpanding here (args: any) => any.
  • We point out that FunctionReturnTypethis is a conditional type.
  • We check if it expands FunctionType (...args: any) => infer ReturnType.

Having done all this, we can extract the return type of any function.

FunctionReturnType<typeof getRandomInteger>; // number

The above is such a common task that TypeScript has a built-in utility ReturnType , which is designed to solve this problem.

Construct as const


Another issue related to type inference is the difference between the keywords constand letwhich are used when declaring constants and variables.

let fruit = 'Banana';
const carrot = 'Carrot';

Variable fruit- has a type string. This means that any string value can be stored in it.

And a constant carrotis a string literal. It can be considered as an example of a subtype string. The following description of string literals is given in this PR : "The string literal type is a type whose expected value is a string with text content equivalent to the same contents of the string literal."

This behavior can be changed. TypeScript 3.4 introduces an interesting new feature called const assertions that provides for the use of a construct as const. Here is what its use looks like:

let fruit = 'Banana' as const;

Now fruitit's a string literal. The design as constis also convenient when some entity needs to be made immutable. Consider the following object:

const user = {
  name: 'John',
  role: 'admin'
};

In JavaScript, the keyword constmeans that you cannot overwrite what is stored in a constant user. But, on the other hand, you can change the internal structure of an object recorded in this constant.

Now the object stores the following types:

const user: {
  name: string,
  role: string
};

In order for the system to perceive this object as immutable, you can use the design as const:

const user = {
  name: 'John',
  role: 'admin'
} as const;

Now the types have changed. Strings became string literals, not ordinary strings. But not only that has changed. Now the properties are read-only:

const user: {
  readonly name: 'John',
  readonly role: 'admin'
};

And when working with arrays, even more powerful possibilities open up before us:

const list = ['one', 'two', 3, 4];

The type of this array is (string | number)[]. Using this array, as constyou can turn it into a tuple:

const list = ['one', 'two', 3, 4] as const;

Now the type of this array looks like this:

readonly ['one', 'two', 3, 4]

All this applies to more complex structures. Consider the example that Anders Halesberg gave in his speech at TSConf 2019 :

const colors = [
  { color: 'red', code: { rgb: [255, 0, 0], hex: '#FF0000' } },
  { color: 'green', code: { rgb: [0, 255, 0], hex: '#00FF00' } },
  { color: 'blue', code: { rgb: [0, 0, 255], hex: '#0000FF' } },
] as const;

Our array is colorsnow protected from changes, and its elements are also protected from changes:

const colors: readonly [
    {
        readonly color: 'red';
        readonly code: {
            readonly rgb: readonly [255, 0, 0];
            readonly hex: '#FF0000';
        };
    },
    /// ...
]

Summary


In this article, we looked at some examples of using advanced type inference mechanisms in TypeScript. The keyword inferand mechanism are used here as const. These tools can be very useful in some particularly difficult situations. For example, when you need to work with immutable entities, or when writing programs in a functional style. If you want to continue familiarization with this topic - take a look at this material.

Dear readers! Do you use keyword inferand construction as constin TypeScript?


All Articles