0

When using spread operator , prevent overwriting keys with new value undefined

Consider an object bleh1 and bleh2

const bleh1 = {
  name: "ajnskdas",
  foo: "oof",
  bar: "something"
}

const bleh2 = {
  foo: "oofElse",
  bar: undefined,
  booz: "chilled"
}

bleh2.bar should overwrite key bar only if value is not undefined

const bleh3 = {...bleh1, ...bleh2}
// Actual
// {
//   "name": "ajnskdas",
//   "foo": "oofElse",
//   "bar": undefined,
//   "booz": "chilled"
// }
// Desired 
// {
//   "name": "ajnskdas",
//   "foo": "oofElse",
//   "bar": "something",
//   "booz": "chilled"
// } 

I can do it during runtime with function removeEmpty but type/interface of bleh4 wont have new keys of bleh2

ie bleh4.booz is not inferred by typescript

function removeEmpty(obj: any) {
  return Object.fromEntries(Object.entries(obj).filter(([_, v]) => v != null));
}
const bleh4 = { ...bleh1, ...removeEmpty(bleh2) }
Solaris
  • 674
  • 7
  • 22
  • 3
    a spreader is a spreader not a merge function, write a merge function – MikeT Jul 27 '22 at 14:24
  • 1
    It looks as if the spread into an object does not source the name/value pairs via an iterator on the source object. There's no default iterator behavior for objects, but even if you use a "special" object that does have an iterator, the spread behavior in an object literal always looks at the raw property keys in the source object. – Pointy Jul 27 '22 at 15:11

2 Answers2

1

the main issue you seem to be having is that your untyped bleh1 and bleh2 are incomparable bleh1 says that bar must be a string, bleh2 says bar must be undefined

when merging the types bar can't be both string and undefined at the same time, which equates to the type never

however if you type bleh1 and 2 then you can tell it how to match the schemas

function merge<T1, T2>(a: T1, b: T2): Partial<T1 & T2> {
    const rtn: Partial<T1 & T2> = { ...a };
    for (const [k, v] of Object.entries(b)) {
        if (v) rtn[k as keyof T2] = v;
    }
    return rtn;
}

const bleh3 = merge(
    {
        name: 'ajnskdas',
        foo: 'oof',
        bar: 'something',
    } as {
        foo: string;
        bar: string | undefined;
        name: string;
    },
    {
        foo: 'oofElse',
        bar: undefined,
        booz: 'chilled',
    } as {
        foo: string;
        bar: string | undefined;
        booz: string;
    }
);
console.log(bleh3);
MikeT
  • 5,398
  • 3
  • 27
  • 43
  • While this approach is giving merged types but consider `foo` as `string` in `bleh1` while `string | undefined` in `bleh2`. As we wont be overwriting with undefined , the type of `foo` after merge should be `string` as atleast 1 object will contain `foo` thus I feel we are still lacking something – Solaris Jul 28 '22 at 11:57
0

As mentioned in the answer by @MikeT , when merging the types bar can't be both string and undefined at the same time, which equates to the type never

Thus the function below works similar , provided by @xor_71 from Typescript Discord

function removeUndefined(obj: any) {
    return Object.fromEntries(Object.entries(obj).filter(([_, v]) => v != undefined));
}

type RemoveUndefinedProps<T extends Record<string, unknown>> = {[key in keyof T]-?: Exclude<T[key], undefined>}


const merge = <T1 extends Record<string, unknown>, T2 extends Record<string, unknown>>(obj1: T1, obj2: T2): 
Omit<T1, keyof T2> 
& {[key in keyof T2 & keyof T1]-?: undefined extends T2[key] ? Exclude<T2[key], undefined> | T1[key] : T2[key]} 
& Omit<RemoveUndefinedProps<T2>, keyof T1> => {
    return {...obj1, ...removeUndefined(obj2)} as any
}

const bleh5 = merge(bleh1, bleh2)

removeUndefined removes any undefined properties from an object RemoveUndefinedProps removes props that can be undefined Thus in the merge function we can use the return type as an intersection (and) of

  • Unique keys from obj1 (T1)
  • Possibly undefined keys
  • Not undefined keys from obj2 (T2)
Solaris
  • 674
  • 7
  • 22