infer
keyword is one of the difficult features that TypeScript offers. Some utility types use infer
keyword, so let’s learn how it is used and how we can utilize it.
In this article, we will look at the following types.
// Get return type
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
// Get parameters of a function
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
ReturnType
3 different callback functions without using infer keyword
How can we implement it if we need to call 3 different functions but all of them require the same procedure before calling the function? One of the solutions is applying Strategy Pattern.
interface ActionExecutorArgs { }
interface Action3ExecutorArgs extends ActionExecutorArgs {
str: string;
num: number;
}
abstract class ActionExecutor<T>{
public execute(args: ActionExecutorArgs): T {
// Do something here
// ...
return this.action(args);
}
protected abstract action(args: ActionExecutorArgs): T;
}
class Action1Executor extends ActionExecutor<number>{
protected action(): number {
return 33;
}
}
class Action2Executor extends ActionExecutor<string>{
protected action(): string {
return "SUPER";
}
}
type Action3ReturnType = { prop1: string, prop2: number };
class Action3Executor extends ActionExecutor<Action3ReturnType>{
protected action(args: Action3ExecutorArgs): Action3ReturnType {
return {
prop1: args.str,
prop2: args.num,
};
}
}
If the functions are small enough, it’s cumbersome to maintain 3 classes to achieve this. Of course, the result is correct as you can see in the code below.
const actions = [
new Action1Executor(),
new Action2Executor(),
new Action3Executor(),
];
actions.forEach((action) => {
const args = action instanceof Action3Executor ? { str: "HELLO", num: 55 } : {};
const result = action.execute(args);
console.log(result);
});
// 33
// SUPER
// { prop1: 'HELLO', prop2: 55 }
Can we somehow write a smarter code?
A function that requires a callback as a parameter
The same thing can be achieved if the function requires a callback and calls it at the end. Let’s define the 3 functions at first.
function action1(): number {
return 33;
}
function action2(): string {
return "SUPER";
}
function action3(str: string, num: number): Action3ReturnType {
return {
prop1: str,
prop2: num,
};
}
We want to call them in a function. The function looks like this.
function doAction0(action: (...args: any[]) => any, ...args: any[]): any {
// do something here
return action(...args);
}
// result is any type
const result = doAction0(action1);
However, using any is not a good choice in TypeScript. Since result
is any data type, IntelliSense doesn’t help us. To improve this, we can define our own data type in the following way.
type ActionReturnType = number | string | Action3ReturnType;
function doAction1(action: (...args: any[]) => ActionReturnType, ...args: any[]): ActionReturnType {
// do something here
return action(...args);
}
// result is ActionReturnType
const result = doAction1(action1);
This code is better than before because IDE can understand that result
property is either number, string, or Action3ReturnType.
However, if we want to pass a different function that has a different return type, we need to add the new type to ActionReturnType
.
We can improve this situation by using infer keyword.
Using ReturnType to extract the return type
Firstly, let’s see how ReturnType
is defined.
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
It’s a bit long. Let’s check one by one.
ReturnType<T extends (...args: any) => any>
This definition means that T is a function.
T extends (...args: any) => infer R
This is almost the same as above. The difference is only the last. The compiler tries to infer the data type of the return value. When looking at the whole right side
T extends (...args: any) => infer R ? R : any
It is a ternary operator. If T is a function, it returns R, otherwise any. If the function returns string, it returns string.
T extends (...args: any) => infer R ? R : any
(...args: any) => string ? string: any;
Let’s use ReturnType
for the 3 function that we defined above.
function action1(): number {
return 33;
}
function action2(): string {
return "SUPER";
}
function action3(str: string, num: number): Action3ReturnType {
return {
prop1: str,
prop2: num,
};
}
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
// number
type Action1Type = ReturnType<typeof action1>;
// string
type Action2Type = ReturnType<typeof action2>;
// { prop1: string; prop2: number; }
type Action3Type = ReturnType<typeof action3>;
We can get the correct data type here.
However, we actually don’t have to define the different types if we improve doAction
function in the following way.
function doAction2<T extends (...args: any[]) => any>(action: T, ...args: any[]): ReturnType<T> {
// do something here
return action(...args);
}
// number
const resultType1 = doAction2(action1)
// string
const resultType2 = doAction2(action2)
// Action3ReturnType
const resultType3 = doAction2(action3, "HELLO", 123)
In this way, we can pass whatever function in a type-safe way without adding the data type.
Parameters
Once software grows, some functions get annoyed because of the long parameter list. If the parameter is not defined as an object, it’s hard to maintain. If it contains a default value and optional parameters and if we don’t want to set some of them but want to set a value to the last value, we have to set them all at all.
function annoyingFunc(param1: number, param2: string, param3 = false, param4?: string, param5?: string) {
console.log(
`param1: ${param1}\n`,
`param2: ${param2}\n`,
`param3: ${param3}\n`,
`param4: ${param4}\n`,
`param5: ${param5}\n`,
);
}
annoyingFunc(111, "HEY", false, undefined, "I want to set only this arg");
If we need to add or change the parameter list and the function is called in many places, it becomes technical debt.
To improve this situation, we can use Parameters
type. It is defined in the following way.
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
infer
keyword is used at the parameter type definition place. It means that it returns the parameter list as an array. More precisely, it is a tuple.
// [
// param1: number,
// param2: string,
// param3?: boolean | undefined,
// param4?: string | undefined,
// param5?: string | undefined
// ]
type AnnoyingFuncParameters = Parameters<typeof annoyingFunc>;
Each element has its own data type. By using this type, we can easily create a utility function that generates the parameter list.
function createFuncArg(args: {
param1: number,
param2: string,
param3?: boolean,
param4?: string,
param5?: string,
}): Parameters<typeof annoyingFunc> {
return [
args.param1,
args.param2,
args.param3 ?? false, // default value must be defined here
args.param4 ?? undefined,
args.param5 ?? undefined,
]
}
// It's not necessary to set unnecessary parameters
// param3 and param4 are not defined
const args = createFuncArg({
param1: 1111,
param2: "HEY",
param5: "optional string"
});
annoyingFunc(...args);
// param1: 1111
// param2: HEY
// param3: false
// param4: undefined
// param5: optional string
In this way, the annoying function was able to be refactored.
Overview
ReturnType and Parameters types are useful to use but I still don’t have a good idea when to use infer keyword. Maybe I’m still not familiar with it and thus I don’t use infer keyword in my project.
I found Parameters type is powerful when refactoring annoying function. If you find such a function, please try to apply the technique that I showed above!
Comments