Home > Net >  Typescript generic mapped service type (change function parameters)
Typescript generic mapped service type (change function parameters)

Time:07-30

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

  • Related