How would I write the equivalent of {...a,...b} as a generic function in typescript 4.4 . I know both a
and b
are records, but don't know what they are in advance. I'd like to create a generic type that constrains arbitrary functions to the {...a,...b} operation.
Example that isn't working:
type Rec = Record<string,unknown>
type ExtendRec = <X extends Rec, Y extends X>(x: X) => Y
// or <X extends Rec, Y extends Rec>(x: X) => Y&X
const addA:ExtendRec = <X extends Rec>(x:X) => ({...x, a: 'a'})
const addB:ExtendRec = <X extends Rec>(x:X) => ({...x, b: 'b'})
const addC:ExtendRec = <X extends Rec>(x:X) => ({...x, c: 'c'})
const blank = {} // expected {}
const a = addA(blank) // expected {a:string}, actual:{}
const ab = addB(a) // expected {a:string,b:string}, actual:{}
const abc = addC(ab) // expected {a:string,b:string,c:string}, actual:{}
The type error on each of the addA, addB,addC functions is:
Type '<X extends Rec>(x: X) => X & { c: string; }' is not assignable to type 'ExtendRec'.
Type 'X & { c: string; }' is not assignable to type 'Y'.
'X & { c: string; }' is assignable to the constraint of type 'Y', but 'Y' could be instantiated with a different subtype of constraint 'Record<string, unknown>'.
The mystifying thing is that it works if I just remove ExtendRec
as function annotations, so typescript already has the ability to infer object assign operations correctly, but I can't write a generic function type that constrains an arbitrary function to that extend operation.
CodePudding user response:
I think this type needs a generic (to capture the properties that will be added), as well as the function (to capture the object being extended).
For example:
type ExtendRec<R extends Record<string, unknown>> = <X>(x: X) => X & R
Now the rest works as you expect:
type ExtendRec<R extends Record<string, unknown>> = <X>(x: X) => X & R
const addA: ExtendRec<{ a: string }> = (x) => ({ ...x, a: 'a'})
const addB: ExtendRec<{ b: string }> = (x) => ({ ...x, b: 'b'})
const addC: ExtendRec<{ c: string }> = (x) => ({ ...x, c: 'c'})
const blank = {} // expected {}
const a = addA(blank) // expected {a:string}
const ab = addB(a) // expected {a:string,b:string}
const abc = addC(ab) // expected {a:string,b:string,c:string}
Or maybe this approach that uses a merge function with generics for both arguments. addA
now just uses that where one argument is the generic and the other is a known type.
type MergeFn = <A extends object, B extends object>(a: A, b: B) => A & B
const merge: MergeFn = (a, b) => ({ ...a, ...b })
const addA = <X extends object>(x: X) => merge(x, { a: 'a'})
const addB = <X extends object>(x: X) => merge(x, { b: 'b'})
const addC = <X extends object>(x: X) => merge(x, { c: 'c'})
This is sort of the same thing as the previous example though. In each case, the added type is declared statically somehow when addA
is declared, and X
is inferred at the call time of addA
.
Note, I'm using object
instead of Record<string, unknown>
because it's shorter, but that's not really too important here.