> 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 })
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" })
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'.
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.
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!
You can do this: