Search code examples
typescripttypingtypeguards

Typescript `in` operator type guard narrowing only as literal


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);
}


Solution

  • Regarding to docs

    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);
    }