I have some object with functions as values, something like this:
const service = {
methodA: (param1: TypeA, otherParam: string) => ({ a: 1 }),
methodB: (param1: TypeA, otherParam: number) => ({ b: 2 }),
}
What I'm trying to achieve is to have some generic function, that would pass the first parameter to each of those methods, creating new service with one less param in each method:
const serviceWithInjectedParam = injectFirstParam(someValue, service)
serviceWithInjectedParam.methodA(otherValue) // this should at the end call service.methodA(someValue, otherValue)
The thing is, that I cannot figure out typings for injectFirstParam
:( This is what I'm trying:
const injectFirstParam = <
S extends { [method: string]: (param1: TypeA, ...params: unknown[]) => unknown }
>(
param1: TypeA,
service: S
) => {
return (Object.keys(service) as (keyof S)[]).reduce((acc, key) => {
acc[key] = (...params: unknown[]) => service[key](param1, ...params) // what should be params type here? I have TS error here
return acc
}, {} as { [key in keyof S]: S[key] extends (param1: TypeA, ...params: infer P) => infer R ? (...params: P) => R : never })
}
but TS says that
Type '(...params: unknown[]) => unknown' is not assignable to type 'S[keyof S] extends (param1: TypeA, ...params: infer P) => infer R ? (...params: P) => R : never'.
and when I try to use injectFirstParam
I'm getting
Argument of type '{ methodA: (param1: TypeA, otherParam: string) => { a: number; }; methodB: (param1: TypeA, otherParam: number) => { b: number; }; }' is not assignable to parameter of type '{ [method: string]: (param1: TypeA, ...params: unknown[]) => unknown; }'.
CodePudding user response:
In this case it is justified to use any
instead of unknown
because arguments are in contravariant position.
type TypeA = string
const service = {
methodA: (param1: TypeA, otherParam: string) => ({ a: 1 }),
methodB: (param1: TypeA, otherParam: number) => ({ b: 2 }),
}
const keys = <Obj extends Record<string, unknown>>(obj: Obj) =>
Object.keys(obj) as Array<keyof Obj>
type Reduce<Service, Type> = {
[Key in keyof Service]:
Service[Key] extends (param1: Type, ...params: infer P) => infer R
? (...params: P) => R
: never
}
const injectFirstParam = <
Service extends Record<PropertyKey, (param1: TypeA, ...params: any[]) => any>
>(
param1: TypeA,
service: Service
) =>
keys(service)
.reduce((acc, key) => ({
...acc,
[key]: <Param, Params extends Param[]>(...params: [...Params]) =>
service[key](param1, ...params)
}), {} as Reduce<Service, TypeA>)
const result = injectFirstParam('hello', service) // ok
result.methodB(2) // ok
See this simplified example:
declare let unknown: unknown
declare let string: string
unknown = string // ok
string = unknown // error
string
is assignable to unknown
and unknown
is not assignable to string
. It is expected bahaviour.
Because function arguments are in contravariant position, arrow of inheritance turns in opposite way.
See this example:
const contravariance = (cb: (arg: unknown) => void) => { }
contravariance((arg: string) => { }) // error
contravariance((arg: unknown) => { }) // ok
As you might have noticed, now, callback with string
argument is not assignable to to unknown
, whereas unknown
is assignable to string
If you are interesting in this topic, please see my question.
Further more, TypeScript does not like mutations. See my article and linked questions/answers