DEV Community

loading...

Discussion on: Indexing objects in TypeScript

Collapse
wdoug profile image
Will Douglas

I recently added some helper utilities to our codebase based on your advice for the hasKey function above. This function works great when you are dealing with object literals (or at least well-defined object types). When the object in question is not well defined (for example, the object is interpreted as the base type object or we don't know the specific keys on the object), then the hasKey function doesn't work as well.

For example, take this function:

function fn(o: object, k: string) {
  if (hasKey(o, k)) {
    o[k] = 3; // Error: Type '3' is not assignable to type 'never'.
  }
}
Enter fullscreen mode Exit fullscreen mode

In the above code, the key k gets narrowed to the type never because keyof object is never (since the object type doesn't have any defined keys).

There is, however, a different way to handle this case that uses a type predicate to update the type for the object itself. I found it in this other blog post (fettblog.eu/typescript-hasownproperty), and I have copied a slightly modified version of the code below for reference:

function hasOwnProperty<O extends object, K extends PropertyKey>(
  obj: O,
  key: K,
): obj is O & Record<K, unknown> {
  return Object.prototype.hasOwnProperty.call(obj, key);
}
Enter fullscreen mode Exit fullscreen mode

Side note that PropertyKey is an included TypeScript es5 lib global type definition that is equavalent to string | number | symbol.

With this helper, this code will work fine:

function fn(o: object, k: string) {
  if (hasKey(o, k)) {
    o[k] = 3; // This is fine
  }
}
Enter fullscreen mode Exit fullscreen mode

Additionally, because of how TypeScript handles intersections with the unknown type, this also works well for cases where the object is more well defined.

const k = 'a';
const o = { a: 0 };
if (hasOwnProperty(o, k)) {
  const result = o[k];
  console.log(result); // typeof result is number
}
Enter fullscreen mode Exit fullscreen mode

While the approach of hasKey for narrowing the key type itself probably still has its uses, I currently think that when it comes to indexing objects, the approach used for the hasOwnProperty utility is probably more broadly applicable and preferable.

Collapse
wdoug profile image
Will Douglas

If you want to be extra fancy and not add the intersection with Record<K, unknown> if the key is known to exist on the object type, you can also use conditional types. For example:

export function hasOwnProperty<O extends object, K extends PropertyKey>(
  obj: O,
  key: K,
): obj is ObjectWithKey<O, K> {
  return Object.prototype.hasOwnProperty.call(obj, key);
}

/**
 * This utility type takes an object type O and a key type K. If K is a known 
 * key on O, it evaluates to the original object O. Otherwise, it evaluates to 
 * the object O with an additional key K that has an `unknown` value.
 */
type ObjectWithKey<O extends object, K extends PropertyKey> = K extends keyof O
  ? O
  : O & Record<K, unknown>;
Collapse
wdoug profile image
Will Douglas

Actually, the conditional type doesn't work when either of the arguments is itself a generic type (playground example).