Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

> And I don't know how to express the correct solution (i.e. where we actually assert that A and B are object types).

You can do this:

  function merge<
    A extends Record<string, unknown>,
    B extends Record<string, unknown>
  >(a: A, b: B): A & B {
    return { ...a, ...b }
  }

  const result = merge({ a: 1 }, { b: 2 })


This fails on:

  const result = merge({ a: 1 }, { a: "fdsfsd" })
The correct type is quite complex and depends on whether or not `exactOptionalPropertyTypes` is enabled.

EDIT: I think this is correct for when `exactOptionalPropertyTypes` is off.

  type OptionalKeys<T extends { [key in symbol | string | number]?: unknown }> = { [K in keyof T]: {} extends Pick<T, K> ? K : never }[keyof T]

  function merge<
    A extends { [K in symbol | string | number]?: unknown },
    B extends { [K in symbol | string | number]?: unknown },
  >(a: A, b: B): {
    [K in Exclude<keyof B, keyof A>]: B[K]
    } & {
      [K in Exclude<keyof A, keyof B>]: A[K]
    } & {
      [K in keyof A & keyof B]: K extends OptionalKeys<B> ? A[K] | Exclude<B[K], undefined> : B[K];
    }
That's for when `exactOptionPropertyTypes` is enabled. With it disabled, then you'd replace `Exclude<B[K], undefined>` with `B[K]`.

As to whether this is a good idea. Ah... it's not :P


Wow yeah it gets way too complex if you want to track the types of properties within the objects too! If that is the case, then I would just prefer to do this instead as it is much simpler:

  type Value = { a: string }
  const result = merge<Value, Value>({ a: 1 }, { a: "fdsfsd" })


Why Record and not object?


    Avoid the Object and {} types, as they mean 'any non-nullish value'.
    This is a point of confusion for many developers, who think it means 'any object type'.
https://github.com/typescript-eslint/typescript-eslint/blob/...


Linters for TypeScript recommend using `Record<string, any>` instead of `object`, since using the `object` type is misleading and can make it harder to use as intended.

See:

- https://typescript-eslint.io/rules/ban-types/

- https://github.com/typescript-eslint/typescript-eslint/issue...

- https://github.com/microsoft/TypeScript/issues/21732

- https://github.com/microsoft/TypeScript/pull/50666


Because you only want to merge two objects that have keys with string type. "object" is represented as Record<any, any>. That would mean, you can use any type as key. Here is an example:

  function merge<
    A extends object,
    B extends object
  >(a: A, b: B): A & B {
    return { ...a, ...b }
  }

  const result = merge(() => {}, () => {}) // should fail!
  const anotherResult = merge([1, 2], [3, 4]) // should fail!
Which is obviously not what you want.

This table here gives you a good overview of differences between object and Record<string, unknown>: https://www.reddit.com/r/typescript/comments/tq3m4f/the_diff...




Consider applying for YC's Winter 2026 batch! Applications are open till Nov 10

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: