How to Iterate through heterogenous object in Typescript

I have a state object and an update object that will be combined with the state object, though null in the update means delete so I can’t just do {...a, ...b} with them.

const obj = {
    other: new Date(),
    num:5,
    str:"original string"
}

const objUpdate:Partial<typeof obj> = {
    num:6,
    str:"updated string"
}

The task: Iterate through the update object and apply its values to the original object. This is how I ideally would do it:

Object.entries(objUpdate).forEach(([k,v])=>{
    if (v === undefined) return;
    if (v === null){
        delete obj[k]; // <-
        return;
    }
    obj[k] = v; // <-
})

But at the indicated lines I get an error that No index signature with a parameter of type 'string' was found on type '{ other: Date; num: number; str: string; }'. Ideally Typescript would know that k is already keyof typeof objUpdate (edit: here’s possibly why) but I guess I can explicitly indicate that:

Object.entries(objUpdate).forEach(([k,v])=>{
    if (v === undefined) return;
    if (v === null){
        delete obj[k as keyof typeof objUpdate];
        return;
    }
    obj[k as keyof typeof objUpdate] = v; // <-
})

At the indicated line it complains that Type 'string | number | Date' is not assignable to type 'never'. Type 'string' is not assignable to type 'never'.

  1. Is there a way to help Typescript infer typings correctly in this situation?
  2. Is there a better approach to what I am trying to accomplish?

>Solution :

Dynamically deleting keys on an object is both slow in JavaScript and can make typing more difficult in TypeScript. The easiest way to approach this by a good margin would be to tweak the other parts of your code that reference the state so that they check if the value is null or not. So, instead of having a state object of

const obj = {
    other: <someDate>,
    str: <someString>
}

there would be

const obj = {
    other: <someDate>,
    num: null,
    str: <someString>
}

Using this approach, typing will just work, and the state update will be as trivial as {...a, ...b}.

To type the initial state, map the keys to a new object type with null added in.

const obj = {
    other: new Date(),
    num:5,
    str:"original string"
}
type Obj = typeof obj;
type ObjState = {
  [K in keyof Obj]: Obj[K] | null;
};

// ...

const [stateObj, setStateObj] = useState<ObjState>(obj);
// Update:
setStateObj({ ...stateObj, ...objUpdate });

If some part of the code requires the object to be formatted with the null properties removed (such as for a database query), do that by creating a new object when it’s necessary, just before sending the object, rather than changing the state’s shape.

const objWithNullPropertiesRemoved: Partial<Obj> = Object.fromEntries(
  Object.entries(stateObj)
    .filter(([, val]) => val !== null)
);
// send the objWithNullPropertiesRemoved somewhere

Leave a Reply