typescripttype-inferenceindex-signature

If the condition `property in object` is true, why does TypeScript not infer the type of object[property] to be the union type of object's values?


I'm trying to access object[property], and TypeScript is throwing the element implicitly has any type error.

const getValue = (key: string) => {
  const object = {
    property0: 14
    property1: -3
  }

  if (!(key in object)) {
    throw new Error(`Invalid key: ${key}`)
  }

  return object[key]
} 

If we don't know whether property in object is true, we'd have to provide TypeScript with the index signature ourselves. That is, TypeScript should throw an error if we removed the if(!(key in object)) condition from that function, until we manually note the index signature of object.

But it seems like, given we know at compile-time that key is in object when we make it to the return statement, TypeScript should throw no such error for this function, and should instead infer the return type of getValue to be number.

I can resolve the issue by manually noting the index signature of object:

interface ObjectWithOnlyNumberProperties {
  [key: string]: number
}

const getValue = (key: string) => {
  const object: ObjectWithOnlyNumberProperties = {
    property0: 14
    property1: -3
  }

  if (!(key in object)) {
    throw new Error(`Invalid key: ${key}`)
  }

  return object[key]
} 

But it seems like I shouldn't have to, and the type of object[key] should be inferable at compile-time to typeof property0 | typeof property1 since we know that key in object is true.


Solution

  • Copying Romain Laurent's comment, which answers the question perfectly!

    At compile time, TypeScript is dealing with types, not actual objects. By default, { property0: 14, property1: -3 } is typed as { property0: number, property1: number }. A type in TypeScript is a minimal contract accordance, so as far as TypeScript knows, the { property0: number, property1: number } type could refer to an actual object like { property0: 14, property1: -3, property2: "string" }. Obviously, we know in the code that it won't, but TypeScript doesn't know that. And as the above case indicates, object[property] is of type any, since property2 could be of any type, not just string.

    Update: as per geoffrey's comment, if you're using target library es2022 or later, you should also update the above code to use Object.hasOwn(object, property) instead of property in object, to protect against prototype pollution.