Reputation: 1785
I'm not getting the result I expected from narrowing on a union type. Here's a snippet which captures the issue:
interface A {
a : number;
}
interface B {
b : string;
}
const isAnA = (arg : A | B) : arg is A => {
return "a" in arg;
}
const applyFunc = <T>(func : (x : T) => number, arg : T) => {
return func(arg)
}
const doTheThing = (arg : A | B) => {
let f;
if (isAnA(arg)) {
f = (x : A) => x.a * 2;
} else {
f = (x : B) => parseInt(x.b) * 2;
}
return applyFunc(f, arg);
}
What I expected to happen here was that the isAnA()
typeguard would let the compiler know that f
has type (x : A) => number
if arg
is an A
, and type (x : B) => number
if arg
is a B
, allowing applyFunc
to be called on arg
and f
together.
However, I get this from the compiler:
Type '(x: B) => number' is not assignable to type '(x: A) => number'.
Types of parameters 'x' and 'x' are incompatible.
Property 'b' is missing in type 'A' but required in type 'B'.
Is there any way to get this working other than explicitly typeguarding the call to applyFunc
?
Upvotes: 2
Views: 367
Reputation: 51043
Generally speaking, if you want to tell Typescript that two variables have correlated types, i.e. "either arg
is A
and func
accepts A
, or arg
is B
and func
accepts B
", then they need to be properties of some object with a discriminated union type:
{arg: A, func: (x: A) => number} | {arg: B, func: (x: B) => number}
To make it actually work in your code, the discriminated union needs to be built using a distributive conditional type, so the compiler won't complain about the applyFuncWithObject
function. I solved this by making ArgAndFunc
a parameterised type, so the discriminated union above is ArgAndFunc<A | B>
.
type ArgAndFunc<T> = T extends unknown ? {arg: T, func: (x: T) => number} : never
function applyFuncWithObject<T>(obj: ArgAndFunc<T>) {
// or directly: return obj.func(obj.arg)
return applyFunc(obj.func, obj.arg);
}
function doTheThing(arg: A | B) {
let obj: ArgAndFunc<A | B>;
if(isAnA(arg)) {
obj = {arg, func: (x: A) => x.a * 2};
} else {
obj = {arg, func: (x: B) => parseInt(x.b) * 2};
}
return applyFuncWithObject<A | B>(obj);
}
I don't know if this will meet your needs exactly, since it doesn't allow you to pass arg
and func
as two separate arguments anywhere (without writing a generic helper function like applyFuncWithObject
). On the other hand, if you do need to pass arg
and func
together in multiple places then passing a single object instead will probably simplify your code.
Upvotes: 2
Reputation: 370769
One thing to do would be to use the conditional operator instead - TypeScript works best when reassignment is minimized. But there's still the problem that you have the type (x: A => number) | (x: B) => number)
is undesirably separated from the type of the argument (A | B
).
I think the best approach here would be to invoke the function needed inside the narrowed condition, to avoid having to re-narrow later, eg:
const doTheThing = (arg: A | B) => {
return isAnA(arg)
? applyFunc((x: A) => x.a * 2, arg)
: applyFunc((x: B) => parseInt(x.b) * 2, arg);
}
Upvotes: 2