'How to dispatch calculation to relevant function according to input's type?
When writing a function in TypeScript, is there a way to let the function infer what to do according to the input's type?
For example, let's say that I have a function that calculates the maximum value.
In scenario A, the input is a numeric array (i.e.,
type: number[]), and I want it to return the max value. So I could do:const calcMaxArr = (arr: number[]): number => { return Math.max(...arr) // https://stackoverflow.com/a/39106546/6105259 }In scenario B I also want to calculate the max value, but this time my input data is an object, and I want to return the key that corresponds to the largest value. In this case I could do:
const calcMaxObj = (obj: Record<string, number>): string => { return Object.keys(obj).reduce((a, b) => obj[a] > obj[b] ? a : b); //https://stackoverflow.com/a/27376421/6105259 }
While these two functions (calcMaxArr() & calcMaxObj()) work perfectly well, I wonder whether I could unify them somehow under one function calcMax(). That is, I wonder whether calcMax() can infer, from the input's type, whether to defer/dispatch the calculation to calcMaxArr() or to calcMaxObj().
If type: number[] -> calcMaxArr()
If type: Record<string, number> -> calcMaxObj()
Is there a built-in feature in typescript for such kind of a task?
EDIT
I ran across this tweet that demonstrates a similar procedure but in python. For those who know python, this makes a useful analogy in my opinion.
EDIT 2
I've now learned that what I am asking about actually has a name: My calcMax() is a generic function, and one built-in implementation of such generic functions is seen in Common Lisp, for example.
Solution 1:[1]
Does this work for you...
const calcMax = (inputs: number[] | Record<string, number>): number | string => {
if (Array.isArray(inputs)) {
return calcMaxArr(inputs);
}
return calcMaxObj(inputs);
}
const calcMaxArr = (arr: number[]): number => {
return Math.max(...arr) // https://stackoverflow.com/a/39106546/6105259
}
const calcMaxObj = (obj: Record<string, number>): string => {
return Object.keys(obj).reduce((a, b) => obj[a] > obj[b] ? a : b); //https://stackoverflow.com/a/27376421/6105259
}
Illustration
"use strict";
const calcMax = (inputs) => {
if (Array.isArray(inputs)) {
return calcMaxArr(inputs);
}
return calcMaxObj(inputs);
};
const calcMaxArr = (arr) => {
return Math.max(...arr); // https://stackoverflow.com/a/39106546/6105259
};
const calcMaxObj = (obj) => {
return Object.keys(obj).reduce((a, b) => obj[a] > obj[b] ? a : b); //https://stackoverflow.com/a/27376421/6105259
};
console.log(calcMax([10, 8, 12, 3]));
console.log(calcMax({
"10": 10,
"8": 8,
"12": 12,
"3": 3
}));
WYSIWYG => WHAT YOU SHOW IS WHAT YOU GET
Solution 2:[2]
Long story short: what you want to achieve is not possible. TypeScript gets compiled to JavaScript and all data about types is not available in the runtime, it's effectively removed. So you have to implement runtime checks yourself (solution by @Nalin Ranjan is just one way to do it), TypeScript can't do that for you.
However I can suggest one improvement to the solution provided by @Nalin Ranjan - use overloads. The provided solution has a big problem - the return type is always a union type, in other words:
let k = calcMax({twelve: 12, ten: 10});
console.log(k.length); // error even though we expect string
This would fail because type of k is string | number and .length property is only available on strings.
To fix it you should, like I mentioned, use overloads, like this:
function calcMax(arr: number[]): number;
function calcMax(obj: Record<string, number>): string;
function calcMax(arg: any): any {
if (Array.isArray(arg)) {
return calcMaxArr(arg);
}
return calcMaxObj(arg);
}
Now the example above will work properly. In this particular scenario it might look verbose, but let's imagine you have another function, like:
const calcMaxSet = (set: Set<number>): number => {
return 0; // implementation doesn't matter
}
Then you just have to look at signatures. Since both calcMaxSet and calcMaxArr return the same type you can add it to the existing overload, same would be true if calcMaxObj actually returned number (but then you wouldn't even need an overload):
function calcMax(arr: number[] | Set<number>): number;
function calcMax(obj: Record<string, number>): string;
function calcMax(arg: any): any {
if (Array.isArray(arg)) {
return calcMaxArr(arg);
} else if (arg instanceof Set) { // runtime check
return calcMaxSet(arg);
}
return calcMaxObj(arg);
}
let k = calcMax(new Set([1,2,3]));
k.toFixed(); // k is a number so this is legal
Solution 3:[3]
Is function overload what you need?
https://www.typescriptlang.org/docs/handbook/2/functions.html#function-overloads
const calcMaxArr = (arr: number[]): number => {
return Math.max(...arr) // https://stackoverflow.com/a/39106546/6105259
}
const calcMaxObj = (obj: Record<string, number>): string => {
return Object.keys(obj).reduce((a, b) => obj[a] > obj[b] ? a : b); //https://stackoverflow.com/a/27376421/6105259
}
function calcMax(arr: number[]): number;
function calcMax(obj: Record<string, number>): string;
function calcMax(input: number[] | Record<string, number>) {
if(Array.isArray(input)) {
return calcMaxArr(input);
}
else {
return calcMaxObj(input);
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Nalin Ranjan |
| Solution 2 | paolostyle |
| Solution 3 | Zheng Bowen |
