Home > Software design >  TypeScript typing for function that transforms an object whose values are functions?
TypeScript typing for function that transforms an object whose values are functions?

Time:03-27

How would I type the following transformer function?

class MyClass {
  public id: string = ''
}

const instance = new MyClass()

function transformer(funcs) {
  return Object.fromEntries(
    Object.entries(funcs)
     .map(([key, func]) => [key, func.bind(instance)])
  )
}

The crux of my conundrum: More than just passing muster with the linter and compiler, I want intelligent typing for this, such that passing in an object with various string keys bonded to function values (each which expect a this arg of type MyClass) gets transformed such that the output is identical to the input except that it's had its requisite this param "burnt-in," and this is known to the editor/linter/compiler.

In fact, I can't even solve for the simpler solitary case, a function that takes a single param of a function needing a this of type MyClass, along with any number of additional params, and a certain return type… and it spits back a function that's typed identically, except its this has been "burnt-in."

function transform(fn) {
  return fn.bind(new MyClass())
}

Even partial answers or further insight would be helpful here! I'd think that we'd need some clever and deep use of generics here, but don't even exactly know where to start. And any answers that can point to further documentation or reference material on the concepts used are especially appreciated!

CodePudding user response:

You can do this via using mapped types and a conditional type to strip the this param. I also modified your transformer function to be generic on the instance but still require that the function's this argument is the type of the instance provided to avoid code dupe later on. Here is my solution:

class MyClass {
  public id: string = ''
}

const instance = new MyClass()

type StripThisParam<T extends (...args: any[]) => any> =
    T extends (this: any, ...params: infer $RestParams) => any
        ? (...args: $RestParams) => ReturnType<T>
        : T;

function transformer<
    I,
    T extends Record<string, (this: I, ...otherArgs: any[]) => any>
>(instance: I, funcs: T): { [K in keyof T]: StripThisParam<T[K]> } {
  return Object.fromEntries(
    Object.entries(funcs)
     .map(([key, func]) => [key, func.bind(instance)])
  ) as any;
}

const foo = transformer(instance, {
    bar(this: MyClass, name: string, age: number) {
        console.log(this.id);
    }
});

foo.bar("hey", 23);

TypeScript Playground Link

CodePudding user response:

A function type can include the type of this inside the function. For example:

const f: (this: MyClass, x: number) => string =
  function(this: MyClass, x: number): string { /* ... */}

There’s a builtin utility type OmitThisParameter that can remove or ‘burn in’ this this parameter:

/**
 * Extracts the type of the 'this' parameter of a function type,
 * or 'unknown' if the function type has no 'this' parameter.
 */
type ThisParameterType<T> = T extends (this: infer U, ...args: never) => any
  ? U
  : unknown

/**
 * Removes the 'this' parameter from a function type.
 */
type OmitThisParameter<T> = unknown extends ThisParameterType<T>
  ? T
  : T extends (...args: infer A) => infer R ? (...args: A) => R : T

You can type transform and transformer like this:

type MyClassFn = (this: MyClass, ...args: never[]) => unknown

function transform<F extends MyClassFn>(fn: F): OmitThisParameter<F> {
  // requires a type cast here: fn.bind doesn't seem to have an overload
  // for a generic number of parameters with possibly different types
  return (fn.bind as (thisArg: MyClass) => OmitThisParameter<F>)(instance)
}

function transformer<T extends Record<string, MyClassFn>>(
  funcs: T
): {[K in keyof T]: OmitThisParameter<T[K]>} {
  return Object.fromEntries(
    Object.entries(funcs)
     .map(([key, func]) => [key, func.bind(instance)])
  ) as {[K in keyof T]: OmitThisParameter<T[K]>}
}

// Usage
declare const f1: (this: MyClass, x: number, y: string) => number
declare const f2: (this: MyClass, x: number, y: boolean) => string
declare const f3: (this: MyClass) => void

// (x: number, y: string) => number
transform(f1)

// (x: number, y: boolean) => string
transform(f2)

// () => void
transform(f3)

// {
//   f1: (x: number, y: string) => number
//   f2: (x: number, y: boolean) => string
//   f3: () => void
// }
transformer({f1, f2, f3})

Playground link

  • Related