LGenzelis
LGenzelis

Reputation: 834

Typescript: type narrowing not working for `in` when key is stored in a variable

Consider this simple snippet. I'm also pasting it here:

type A =
  | {
      b: number;
    }
  | {
      c: number;
    };

function f1(a: A) {
  if ('b' in a) {
    return a['b']; // No problem!
  }
  return 42;
}

function f2(a: A) {
  const key = 'b';
  if (key in a) {
    return a[key];  // Property 'b' does not exist on type 'A'
  }
  return 42;
}

Why doesn't the type of a get narrowed to {b: number} in f2? (as it is for f1)

Upvotes: 12

Views: 5627

Answers (3)

thehappycheese
thehappycheese

Reputation: 353

In older typescript version 3.6.3 both existing answers above did not work for me. I am stuck in an older version because of my target environment. Please see below my working version:

/**
 * Type guard function to check if a property exists in an object.
 * 
 * @template T - The type of the object. Must extend object.
 * 
 * @param {PropertyKey} key - The property key to check.
 * @param {T} obj - The object in which to check for the property.
 * 
 * @returns {boolean} - True if the property exists in the object, false otherwise.
 * 
 * If true, the TypeScript type of `key` is narrowed to be a keyof `T`, 
 * indicating it exists as a property within the object `T`.
 */
export function is_in<T extends Object>(
    key: PropertyKey, obj: T
): key is keyof T {
    return key in obj;
}

Upvotes: 1

catwith
catwith

Reputation: 1275

You need custom type guard functions like this:

function isin<T>(key: PropertyKey, obj: T): key is keyof T {
  return key in obj;
}

TypeScript doesn't narrow the type of a key stored in a variable, apparently for a performance reason.

For an alternative approach and the detailed reason for why TypeScript doesn't do it, see this answer: https://stackoverflow.com/a/64618261/13651701

Upvotes: 3

jcalz
jcalz

Reputation: 327934

This is essentially the same issue as microsoft/TypeScript#10530; type narrowing from control flow analysis only happens for properties that are directly literals like "b" and not for arbitrary expressions whose types are literal types. Issue #10530 talks about narrowing via property access... like a.b or a["b"], which does cause a to be narrowed, vs a[key], which does not.

As you've noticed, this also happens with the in operator type guard (as implemented in microsoft/TypeScript#15256), where "b" in a narrows the type of a, but key in a does not. This is not explicitly mentioned in #10530 (which pre-dates the in type guard) but I don't think there's another existing issue specifically about this.

According to microsoft/TypeScript#10565, an initial attempt to address the issue in #10530, adding type guard functionality for arbitrary expressions of literal types significantly worsens the compiler performance. Maybe performing extra analysis for all x in y checks would be less expensive than performing extra analysis for all y[x] property accesses, but at least so far nobody has cared much.

You could always open your own issue about it in GitHub (many issues end up being duplicates, and I'm not 100% sure this wouldn't just be considered a duplicate of #10530, or that there isn't some other issue this duplicates), but practically speaking it's probably not going to change anytime soon.


If you want a workaround for the case where you can't just replace key with a string literal, you could write your own user-defined type guard function called hasProp(obj, prop). The implementation would just return prop in obj, but its type signature explicitly says that a true result should cause obj to be narrowed to just those union members with a key of type prop:

function hasProp<T extends object, K extends PropertyKey>(
    obj: T, prop: K
): obj is Extract<T, { [P in K]?: any }> {
    return prop in obj;
}

and then in your function, replace key in a with hasProp(a, key):

function f3(a: A) {
    const key = 'b';
    if (hasProp(a, key)) {
        return a[key];  // okay
    }
    return 42;
}

Playground link to code

Upvotes: 12

Related Questions