Does type narrowing with the in
operator only work for literals or am I missing something?
Help me understand why this is happening please.
interface A {
a: string;
}
interface B {
b: number;
}
// This narrows the type correctly to A
function test(arg: A | B): string {
if ('a' in arg) {
return arg.a;
}
return arg.b.toFixed(2);
}
// This doesn't
function test2(arg: A | B): string {
let a = 'a';
if (a in arg) {
return arg.a;
}
return arg.b.toFixed(2);
}
For a n in x expression, where n is a string literal or string literal type and x is a union type, the “true” branch narrows to types which have an optional or required property n, and the “false” branch narrows to types which have an optional or missing property n.
So, I'd willing to bet it works only with literals
Workarounds
interface A {
a: string;
}
interface B {
b: number;
}
// This narrows the type correctly to A
function test(arg: A | B): string {
if ('a' in arg) {
return arg.a;
}
return arg.b.toFixed(2);
}
const isIn = <T, Prop extends string>(obj: T, prop: Prop): obj is T & Record<Prop, unknown> => prop in obj
const isIn2 = <T, Prop extends string>(obj: T, prop: Prop): obj is T & Record<Prop, unknown> =>
Object.prototype.hasOwnProperty.call(obj, prop)
// This doesn't
function test2(arg: A | B): string {
const a: 'a' = 'a' as 'a';
if (isIn(arg, a) /** OR isIn2(arg, a) */) {
return arg.a; // A
}
return arg.b.toFixed(2);
}