How to create TypeScript declarations for a function that maps an object with function values?

I'm trying to figure out how to implement the transformer function in JavaScript. This function is supposed to take an object that contains various string keys bonded to functions, and transform it such that the functions expect a 'this' argument of type 'MyClass'.

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

My challenge lies in achieving intelligent typing for this function so that the editor/linter/compiler recognizes that the output maintains the same structure as the input but with the necessary 'this' parameter modified. I'm also struggling with a simpler case where I need to create a function that binds a single function needing a 'this' of type 'MyClass' along with additional parameters and return types.

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

I would greatly appreciate any pointers or insights on how to approach this problem. Generics may play a key role here, but I'm unsure where to begin. Any suggestions for further reading materials on this topic would be highly valued!

Answer №1

To accomplish this task, you can utilize mapped types and a conditional type to remove the this parameter. I have also made adjustments to your transformer function to make it generic based on the instance while still ensuring that the function's this argument matches the type of the instance provided in order to avoid redundancy later on. Here is the revised 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

Answer №2

Inside a function type, the type of this can be specified. For instance:

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

There is a predefined utility type called OmitThisParameter which can eliminate or preserve the this parameter:

/**
 * Retrieves the type of the 'this' parameter from a function type,
 * or 'unknown' if it is not present.
 */
type ThisParameterType<T> = T extends (this: infer U, ...args: never) => any
  ? U
  : unknown

/**
 * Excludes 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 define transform and transformer like this:

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

function transform<F extends MyClassFn>(fn: F): OmitThisParameter<F> {
  // a type assertion is needed here: fn.bind does not support multiple 
  // parameters with different types in a generic way
  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

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

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

// output will be: () => void
transform(f3)

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

Playground link

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Identify the moment the user begins or ends typing using jQuery

Once upon a time, I encountered an issue that required me to detect when a user was typing and when they stopped typing so I could update the status accordingly. I have created a simple solution that may work for you as well. var typingTimer; var doneTyp ...

What is the process of converting the new syntax of SomeFunction() to TypeScript?

When I try to convert a basic JS file to TS while having implicit "any" disabled, I encounter the following error: Error TS7009: When attempting to create a new expression without a constructor signature, it implicitly defaults to an 'any' typ ...

Use the rowTemplate in a Kendo grid without replacing the existing one

I am currently utilizing Angular 1.4 with TypeScript and Kendo UI (employing angular directives). My goal is to create a RowTemplate for each row that dynamically changes the color based on a specific property of the item. While I am aware of jQuery solu ...

Understanding TypeScript: Utilizing type intersection and the powerful 'this' keyword

Can you explain the contrast between the following: interface MyType { f<T>(other: T): this & T; } versus interface MyType { f<T>(other: T): MyType & T; } ? Your insights would be highly appreciated! ...

Typescript compilation fails to include require statements in certain files

Currently, I am in the process of converting a Node.js project to TypeScript. The first two main files of the Node project transpiled correctly, but the third file ran into issues with the requires statement at the top for pulling in dependencies. Despite ...

Differences between Typescript, Tsc, and Ntypescript

It all began when the command tsc --init refused to work... I'm curious, what sets apart these three commands: npm install -g typescript npm install -g tsc npm install -g ntsc Initially, I assumed "tsc" was just an abbreviation for typescript, but ...

Error: The element 'scrollable' is not recognized in Angular2

I recently updated my angular2 project to the final release after previously using angular2 RC5 for development. However, I encountered an error message stating "scrollable is not a known element." If I change <scrollable><!--- angular code -- ...

Remove properties that are not part of a specific Class

Is there a way to remove unnecessary properties from the Car class? class Car { wheels: number; model: string; } const obj = {wheels:4, model: 'foo', unwanted1: 'bar', unwantedn: 'kuk'}; const goodCar = filterUnwant ...

Definition of a generator in Typescript using an interface

I am in the process of converting some code to TypeScript which currently looks like this: const saga = function* (action) { yield put({ type: actions.SUCCESS, payload: action.payload }); }; const sagaWatche ...

Adding curly braces around values when using Angular's form group patchValue method

I have been working on a dynamic form using form builder in order to update form data dynamically. I have created a function that iterates through keys in an object and patches the form values based on matching key names. While this approach works for most ...

Understanding Vue.js - encountering the error message "property or method is not defined"

Recently, I've come across an issue that seems to be common among many people, but for some reason, I am unable to find a solution despite looking at similar questions. The problem arises when I use a v-for in a Vue Component and the array value cons ...

Is there a specific side effect that warrants creating a new Subscription?

Recently, I had a discussion on Stack Overflow regarding RxJS and the best approach for handling subscriptions in a reactive application. The debate was whether it's better to create a subscription for each specific side effect or minimize subscriptio ...

An error has occurred while trying to declare Symbol InputText during an Angular production build

Currently, I am facing an issue while trying to export the primeng modules from a file. During the test build, I do not encounter any errors. However, when I proceed with the production build, I come across the following error: ERROR in Symbol InputText de ...

When I try to add data in Typescript, I am receiving an undefined value

I'm currently dealing with a state in my code: const [file, setFile] = React.useState() This is the function I use to set the state: const fileselectedHandler = (event: any): void => { setFile((event.target.files[0])) } However, I enco ...

The Observable constructor in Nativescript must be called with the 'new' keyword in order to be invoked

I am facing a challenge while attempting to upload a multipart form in nativescript using http-background. The error message "Class constructor Observable cannot be invoked without 'new'" keeps appearing. I have tried changing the compilerOptions ...

Every checkbox has been selected based on the @input value

My component has an @Input that I want to use to create an input and checkbox. import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; @Component({ selector: 'app-aside', templateUrl: './aside.component ...

Issue "TS2339: Could not find attribute 'X' on type 'Y'" encountered while accessing a data property within a function

New to Vuejs 3 and Typescript here. I have a button that should fetch tasks for a "to do" list when clicked. Below is the script I'm using: <script lang="ts"> import axios from 'axios'; export default { name: 'Todos&a ...

What are the steps to successfully launch a Node.js / Express application with typescript on heroku?

I attempted to deploy my node.js / express server app on Heroku but encountered some issues. I followed the steps outlined in a blog post, which you can find here. Unfortunately, the deployment did not succeed. Below are snippets of the code from my serve ...

Error in TypeScript when utilizing generic callbacks for varying event types

I'm currently working on developing a generic event handler that allows me to specify the event key, such as "pointermove", and have typescript automatically infer the event type, in this case PointerEvent. However, I am encountering an error when try ...

Converting an array of objects into a TypeScript dictionary with IDs as the key mapping

Is there a way to provide type hints for better autocompletion when accessing keys like dictionary.Germany from the following data and types? type Entry = { tld: string; name: string; population: number; }; const data: Entry[] = [ {tld: 'de&a ...