Leo Jiang
Leo Jiang

Reputation: 26085

Typescript can't assign value to object key with hasOwnProperty type narrowing

I defined hasOwnProperty with type narrowing:

function hasOwnProperty<
  Obj extends Record<string, any>,
  Prop extends PropertyKey,
>(
  obj: Obj,
  prop: Prop,
): obj is Obj & Record<Prop, any> {
  return Object.prototype.hasOwnProperty.call(obj, prop);
}

I expected a straightforward function like this to work:

function addProp<T>(obj: Record<string, any>, key: string, val: any) {
  if (!hasOwnProperty(obj, key)) {
    obj[key] = val;
  }
}

However, I'm getting Type 'any' is not assignable to type 'never'.

TS Playground

This kind of makes sense because the type narrowing is saying that key doesn't exist in obj. However, this shouldn't prevent me from adding the key. Is there something I'm doing wrong?

Edit: Also, it doesn't work outside of a function either:

const obj: Record<string, number> = {};
const key = 'key';
if (!hasOwnProperty(obj, key)) {
  obj[key] = 123;
}

Upvotes: 2

Views: 372

Answers (1)

function hasOwnProperty<
  Obj extends Record<string, any>,
  Prop extends PropertyKey,
>(
  obj: Obj,
  prop: Prop,
): obj is Obj & Record<Prop, any> {
  return Object.prototype.hasOwnProperty.call(obj, prop);
}


function addProp<T>(obj: Record<string, any>, key: string, val: any) {
  if (!hasOwnProperty(obj, key)) {
    obj // <---------------- is infered to never
    obj[key] = val;
  }
}

In your case, obj after condition statement infered to never. Why?

Because type of key is a string and !hasOwnProperty(obj, key) means that obj has no more string keys.

How to fix it ?

You can just add explicit generic for key argument.

function addProp<T, Prop extends string>(obj: Record<string, any>, key: Prop, val: any) {
  if (!hasOwnProperty(obj, key)) {
    obj[key] = val;
    return obj
  }
  return obj
}


const x = addProp({}, 'a', 42) // Record<string, any> & Record<"a", any>

Here, in my blog, you can find more issues with mutations in typescript

@captain-yossarian's answer works for functions where I can have a generic for the key. However, in places where I can't use a generic, it still doesn't work (see edit)

This example just can't work:

const obj: Record<string, number> = {};
const key = 'key';
if (!hasOwnProperty(obj, key)) {
  obj[key] = 123;
}

SInce you assume that key property is optional in obj, you should use Partial:

const obj: Partial<Record<PropertyKey, number>> = {};
const key = 'key';
if (!hasOwnProperty(obj, key)) {
  const x = obj
  obj[key] = 123; // ok
}

Upvotes: 1

Related Questions