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);
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})