Let's say I have following types:
type a = (a: number) => Promise<string>
type b = (b1: number, b2: string) => number | undefined
And I would like to do something like this:
function test(arg: a | b) {
if(typeof arg === a) {
//....
} else if(typeof arg === b) {
//....
}
}
How can I do something like this?
CodePudding user response:
In TypeScript, the types only exist until the compiler transforms your TS source code into JavaScript. During compilation, types are erased, so they don't exist at runtime — meaning that you can't use them to make decisions in your program's runtime logic (e.g. in conditionals).
Therefore, in order to determine which type of function is being provided for the same argument, you'll need to accept some additional argument that you can use at runtime to make that decision (because functions don't have any discriminable features that would help you do that otherwise). Here are a couple of ways to do that:
For completeness, here the types from your question again:
type A = (a: number) => Promise<string>;
type B = (b1: number, b2: string) => number | undefined;
One way:
Instead of accepting just a function argument, you can accept an object (a discriminated union) that has the function on one property and the type of the function on another property.
type Options =
| { fn: A; fnType: 'a'; } // fn type A is associated with "a"
| { fn: B; fnType: 'b'; }; // fn type B is associated with "b"
Then, because you established those relationships, you can use the fnType
property in a conditional and the compiler will know which associated function type is present in each conditional scope:
function test1 ({fn, fnType}: Options) {
if (fnType === 'a') {
const result = fn(2);
//^? const result: Promise<string>
}
else {
// The only remaining possibility is B, and TS can infer that:
fnType;
//^? (parameter) fnType: "b"
const result = fn(2, 'hello');
//^? const result: number | undefined
}
}
Another way:
If you don't like the idea of using an object argument, you can use two separate arguments with a function overload signature. This method also provides some helpful IntelliSense when being used (see screenshots from the TS playground below the code), but it also means that the relationships between the arguments aren't clearly established within the implementation signature, so you'll have to help the compiler understand by using a type assertion:
function test2 (fnType: 'a', fn: A): void; // The first signature
function test2 (fnType: 'b', fn: B): void; // The second signature
function test2 (fnType: 'a' | 'b', fn: A | B) { // The implementation signature (the return type is not necessary here)
if (fnType === 'a') {
// In the implementation signature,
// there's no defined relationship between the fnType and the fn itself:
fn;
//^? (parameter) fn: A | B
// So an assertion is needed to inform the compiler of that relationship:
const result = (fn as A)(2);
//^? const result: Promise<string>
}
else {
const result = (fn as B)(2, 'hello');
//^? const result: number | undefined
}
}
IntelliSense provides helpful suggestions: