0

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.

  • Because at compile time Typescript is dealing with type not actual object : and 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 `{ property0: number, property1: number }` type could refer to Actual Object like `{ property0: 14, property1: -3, property2: "string" }`. Obviously We knows that in your code it won`t, but TS don't. – Romain LAURENT Jan 14 '23 at 19:55

1 Answers1

2

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.

  • 1
    This is unrelated to your TS issue and the workaround you found, but know that it is not true that "we know that it won't" https://tsplay.dev/Nd26XW – geoffrey Jan 15 '23 at 20:58
  • Hmm—how do you mean? On that TSPlay, I see the error `Property '__proto__' does not exist on type '{}'.` rather than the output `KABOOM!` Afaik, the __proto__ initializer only applies to the object for which it was set (`unrelated`), not for generic objects? So even if I change the first couple lines to `const unrelated = { __proto__: { boom: 'KABOOM!' } }` and the code compiles, it'll just exit with the expected `Invalid key: boom`. If there were a way in JS to set the default prototype for all objects, we indeed wouldn't "know that it won't", but I don't believe JS has that functionality? – Colin Parsons Jan 16 '23 at 21:46
  • if you run the code, you will see `"KABOOM"` in the console. That's the problem. It's called prototype pollution and it is as bad as you probably now realise it is. One way to guard from it is to use `Object.hasOwn(object, key)` in place of `key in object` because you will be sure to check for own properties and exclude properties inherited from the prototype. You can also create naked objects like so `Object.assign(Object.create(null), {foo: 1, bar: 2})`: such objects do not have prototypes, contrary to object literals, and are immune to prototype pollution. – geoffrey Jan 16 '23 at 22:32
  • Note that currently `Object.hasOwn` has no useful type declaration: it returns `boolean` so there will be no type narrowing, but it should come "soon". In the meantime you can patch it https://tsplay.dev/wQYMJw – geoffrey Jan 16 '23 at 22:41
  • Makes sense! Obviously `Object.hasOwn` won't work for older target libraries, but updating the answer to reflect that this is the right approach for `es2022` or later. Thanks! – Colin Parsons Jan 16 '23 at 23:03
  • `Object.hasOwn` has pollyfills https://tsplay.dev/w6Lyrm. I did not try to transpile to it yet. I saw a babel plugin but it relies on `Object.prototype.hasOwnProperty` which could itself be redefined so I don't recommend it. – geoffrey Jan 17 '23 at 09:54