Search code examples
typescripttype-safety

Typesafe item removal from an array in an object - is it possible in a single function without assertion?


I ask this mostly because I'm curious. The title is maybe a little complicated so... Let's say I have an object like this:

type ObjType = { items: Array<{ id: number }>; sth: number };
const obj: ObjType = {
  sth: 3,
  items: [{ id: 1 }, { id: 2 }, { id: 3 }]
};

Now I would want to remove an item from obj.items by id, let's say 2 and return the whole object with that item removed (new object, not mutated). This is generally straightforward in JavaScript, but I would want to do it with type safety preserved in TypeScript. I came up with something like this:

function removeItemFromArray<S, A extends { id: number }>(
  obj: S,
  field: keyof S,
  array: A[],
  valueToCompare: number
): S {
  return {
    ...obj,
    [field]: array.filter(i => i.id !== valueToCompare)
  };
}

but the array param seems redundant - it will always be obj[field]. But if I just used obj[field] there's no guarantee that it's an array. Now the question is how to make sure that obj[field] is an array of objects that contain id property? I'm pretty sure it's possible with conditional types but I can't quite figure it out.


Solution

  • Conditional types won't help here, but you can get what you want by ensuring that obj has the right shape:

    function removeItemFromArray<S extends { [f in F]: { id: number }[] }, F extends keyof S>(
      obj: S,
      field: F,
      valueToCompare: number
    ): S {
      return {
        ...obj,
        [field]: obj[field].filter(i => i.id !== valueToCompare)
      };
    }
    
    const obj = {
      sth: 3,
      items: [{ id: 1 }, { id: 2 }, { id: 3 }]
    };
    
    removeItemFromArray(obj, "items", 2);