1

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?
BobtheMagicMoose
  • 2,246
  • 3
  • 19
  • 27
  • How dynamic are the keys of the object? For example, could there ever be anything other than `other`, `num`, and `str` in the `objUpdate`? (if so, that greatly complicates things - but hopefully not) – CertainPerformance Feb 15 '23 at 20:23
  • They should be static. I am only modifying it here. – BobtheMagicMoose Feb 15 '23 at 20:27
  • Well, to be more precise: They should be fully defined by the type. For example the object satisfies ObjectType but there may be some optional properties in ObjectType that may or may not be present in object. – BobtheMagicMoose Feb 15 '23 at 20:33

2 Answers2

1

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
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
0

The answer I accepted is most likely the correct approach for most people but I just found a way to get my original approach to work and provide it here for completeness.

Using Transform union type to intersection type I then typed the value v as as UnionToIntersection<typeof obj[keyof typeof obj]>

BobtheMagicMoose
  • 2,246
  • 3
  • 19
  • 27