Monday, 16 March, 2020 UTC


Summary

TypeScript works in a way that automates a lot of the work for us. We don’t have to write types every time, because the compiler works hard to derive them from the context. In this article, we look into more complex cases that involve the infer keyword and const assertions.
The basics of type inference
First, let’s look into some elementary examples of type inference.
let variable;
Variable defined in such a way has a type of 
any
. We didn’t give the compiler any tips on how we will use it.
let variable = 'Hello!';
This time, we defined an initial value for our variable. TypeScript can figure out that it is a 
string
, and therefore we now have a adequately typed variable.
A similar thing happens with functions.
function getRandomInteger(max: number) {
  return Math.floor(Math.random() * max);
}
In the code above, we don’t have to indicate that our 
getRandomInteger
 function returns a number. The TypeScript compiler is fully aware of it.

Inference in generics

Powerful usage of the above concept is built into generics.
If you want to know more about generics, check out TypeScript Generics. Discussing naming conventions and More advanced types with TypeScript generics
When creating generic types, we can do lots of useful stuff. Type inference makes it more elegant and easier to use.
function getProperty<ObjectType, KeyType extends keyof ObjectType>(
  object: ObjectType, key: KeyType
) {
  return object[key];
}
When using the above generic function, we don’t have to pass the types explicitly.
const dog = {
  name: 'Fluffy'
};
getProperty(dog, 'name');
The above is also very useful when creating generic React components. If you want to know more, check out Functional React components with generic props in TypeScript
The use-case of the infer keyword
One of the more advanced features that come to mind when discussing inference is the 
infer
 keyword.
First, let’s declare such a function:
function call<ReturnType>(
  functionToCall: (...args: any[]) => ReturnType, ...args: any[]
): ReturnType {
  return functionToCall(...args);
}
Above, we call a function and return its value.
const randomNumber = call(getRandomInteger, 100);
The above gives us the return value of the 
getRandomInteger
 function provided with the maximum value of 100. There is a minor problem with the above, though. Nothing prevents us from not respecting the types of the arguments of the 
getRandomInteger
.
const randomNumber = call(getRandomInteger, '100'); // no error here
Since TypeScript supports spread and rest parameters in higher-order form, we can resolve the above issue.
function call<ArgumentsType extends any[], ReturnType>(
  functionToCall: (...args: ArgumentsType) => ReturnType, ...args: ArgumentsType
): ReturnType {
  return functionToCall(...args);
}
Now we say that the 
call
 function can handle an array of arguments in any form, but it has to match the provided function.
const randomNumber = call(getRandomInteger, '100');
Argument of type ‘”100″‘ is not assignable to parameter of type ‘number’.
In fact, by doing the above, we’ve just created a tuple. In TypeScript, tuples are fixed-length arrays whose types are known but don’t have to be the same.
type Option = [string, boolean];
const option: Option = ['lowercase', true];

Introducing the infer keyword

Now, imagine that instead of getting the return value of a function, we only want to get the return type.
type FunctionReturnType<FunctionType extends (...args: any) => ?> = ?;
The above type is still incomplete. We need to figure out a way to determine the return value. We could be explicit with it, but that would defeat the point.
type FunctionReturnType&lt;ReturnType, FunctionType extends (...args: any) =&gt; ReturnType&gt; = ReturnType;
FunctionReturnType<number, typeof getRandomInteger>;
Instead of doing it explicitly, we can ask TypeScript to infer the return type for us. The infer keyword is permitted only in the conditional types. This is the reason our code sometimes can get a bit messy.
type FunctionReturnType<FunctionType extends (args: any) => any> = FunctionType extends (...args: any) => infer ReturnType ? ReturnType : any;
In the above code, we:
  • state, that the FunctionType extends 
    (args: any) => any
  • we say that the FunctionReturnType is a conditional type
    • inside we declare the ReturnType and assign it with the return type of our function
      • () => infer ReturnType
         results in assigning the return type to the ReturnType variable
  • we check if FunctionType extends 
    (...args: any) => infer ReturnType
    • if the above condition is not met, we assign 
      any
       to FunctionReturnType
    • since the above condition is always met, we assign the ReturnType to the FunctionReturnType
By doing all of the above, we can extract the return type of any function.
FunctionReturnType<typeof getRandomInteger>; // number
The above is such a common case that TypeScript has a built-in utility type called ReturnType that works in the same manner.
Const assertions
Another thing regarding the inference is the difference between the 
const
 and 
let
 variable declaration.
let fruit = 'Banana';
const carrot = 'Carrot';
Our 
fruit
 is a string. It means that it can hold any string value.
Our 
carrot
 is a string literal. We can consider it as a subtype of a string.  This pull request describes it as follows:
A string literal type is a type whose expected value is a string with textual contents equal to that of the string literal type.
We can alter the above behavior. TypeScript 3.4 introduces a new interesting feature called const assertions.
let fruit = 'Banana' as const;
Now our 
fruit
 is a string literal. The const assertions also come in handy when implementing immutability. Let’s consider the following object:
const user = {
  name: 'John',
  role: 'admin'
};
In JavaScript, 
const
 means that we can’t reassign the value that the 
user
 variable holds. We can, on the other hand, modify the object.
Currently, the object holds the following type:
const user: {
  name: string,
  role: string
};
We can use const assertion to treat is as immutable.
const user = {
  name: 'John',
  role: 'admin'
} as const;
Now our type changed. Our strings changed to string literals instead of strings. Not only that, but all of the properties are also
readonly
.
const user: {
  readonly name: 'John',
  readonly role: 'admin'
};
It gets even more powerful with arrays.
const list = ['one', 'two', 3, 4];
The above array has the type of 
(string | number)[]
. We can make it into a tuple with a const assertion:
const list = ['one', 'two', 3, 4] as const;
Now instead of a regular array, our list has a type of 
readonly ['one', 'two', 3, 4]
.
The above behavior also applies to more nested structures. Let’s consider the example that Anders Hejlsberg used in his TSConf 2019 talk:
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 
colors
 array is now deeply immutable:
const colors: readonly [
    {
        readonly color: 'red';
        readonly code: {
            readonly rgb: readonly [255, 0, 0];
            readonly hex: '#FF0000';
        };
    },
    /// ...
]
Summary
In this article, we’ve gone through some more advanced examples of type inference. It included the infer keyword and the const assertions. They can come in handy in some more sophisticated cases. It might prove to be useful, for example, when dealing with immutability and doing functional programming. For more fundamental examples, check out this Type Inference Guide by Tomasz Ducin.
The post TypeScript type inference with const assertions and the infer keyword appeared first on Marcin Wanago Blog - JavaScript, both frontend and backend.