What is the best way to take any constructor type and transform it into a function type that can take the same arguments?

In the code snippet below, a class is created with a constructor that takes an argument of a generic type. This argument determines the type of the parameter received by the second argument. In this case, the first parameter sets the callback function's parameter type to MouseEvent:

class MyClass<T extends keyof HTMLElementEventMap>{
    constructor(tagName: T, listener: (ev: HTMLElementEventMap[T]) => any) { }
}

new MyClass("click", ev => { })
//                   ^^ (parameter) ev: MouseEvent

However, creating a function with rest arguments of type ConstructorParameters<typeof MyClass> results in resolving the callback parameter type as

Event | UIEvent | AnimationEvent | MouseEvent | FocusEvent | DragEvent | ErrorEvent | PointerEvent | ... 6 more ... | ClipboardEvent
.

function myFunction(...args: ConstructorParameters<typeof MyClass>) {}

myFunction("click", ev => { })
//                  ^^ (parameter) ev: Event | UIEvent | AnimationEvent | MouseEvent | FocusEvent | DragEvent | ErrorEvent | PointerEvent | ... 6 more ... | ClipboardEvent

How can I ensure the correct type for the callback parameter without having to redefine types within the myFunction function?

Any suggestions on achieving this?

Answer №1

What you require is to specify "take any constructor type and convert it to a function type that accepts the same arguments".


An issue arises because TypeScript's type system lacks a proper way to define "the arguments of something callable or constructible" that works for generic functions. For instance, if you have a function like:

function foo<T>(x: T, y: T): void { }

and attempt to extract its parameter list, the generic parameter T will be replaced with its constraint. In this case, T is unfettered, and thus has an implicit constraint of unknown:

type FooParams = Parameters<typeof foo>;
// type FooParams = [x: unknown, y: unknown]

TypeScript does not possess appropriate generic types to represent the parameter list of a generic function. A generic function has its generic type parameters on the call signature. However, a tuple type like [x: unknown, y: unknown] lacks a call signature and cannot hold a generic type parameter:

// Invalid in TS, do not utilize this:
type FooParams = <T>[x: T, y: T];

To address this, TypeScript would need something akin to arbitrary generic value types, as requested in microsoft/TypeScript#17574... but such a feature is absent.


Instead of focusing on a tuple type, perhaps we could automatically convert one generic function type into another. Unfortunately, once more, the language lacks suitable type operators for this task. To capture the relationship between a generic function and its type parameter, TypeScript would likely need something like "higher kinded types", as requested in microsoft/TypeScript#1213... but these are also nonexistent.


Prior to TypeScript 3.4, there may have been no solution. Nevertheless, Typescript 3.4 introduced support for higher order type inference from generic functions. While not a full implementation of higher kinded types, it enables transforming an actual generic function value into another generic function value, where the output function's type links to the input function's type precisely as needed. Although there is no available type-level syntax for using this feature, one can derive the desired type by inferring from a pseudo-function at the value level. An example for a class MyClass is demonstrated below:

// taking advantage of TS3.4 support for higher order inference from generic functions
const ctorArgs = <A extends any[], R>(f: new (...a: A) => R): (...a: A) => void => null!
const myFunc = ctorArgs(MyClass)
type MyFunctionType = typeof myFunc;

const myFunction: MyFunctionType = (...args) => {}
myFunction("click", ev => { ev.buttons })

Although ctorArgs accomplishes the intended type manipulation at the value level, it involves including unnecessary JavaScript code in the process. If the goal is solely focused on typing, this approach introduces unneeded complexity.

Considering the unavoidable inclusion of JavaScript code, one might leverage it to implement myFunction generically. The end goal being to take a constructor and transform it into a void-returning function - potentially discarding its result. This concept is exemplified here:

const makeMyFunction = <A extends any[], R>(
  f: new (...a: A) => R): (...a: A) => void =>
    (...a) => { new f(...a) } // whatever the implementation should do

const myFunction = makeMyFunction(MyClass);

In this scenario, myFunction is obtained effortlessly. Nonetheless, its suitability hinges on the specific use case which remains undisclosed.


Link to Playground for Code

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

What causes the createResource error to become undefined when it is refetched before being properly set?

How can I access and display the error property for a createResource? In this scenario, why is the error initially set to undefined before it is thrown? The logging shows that it first displays undefined for the error before eventually getting to line er ...

How to prevent unnecessary new instances from being created by the Inject() function in Angular

Can someone please clarify if the inject() function provides different instances of a service? I suspect this might be why my code is not functioning as expected. Let's examine the code snippet below: { path: 'recipes', comp ...

Sharing code between a node.js server and browser with Typescript: A step-by-step guide

I have an exciting project in mind to develop a multiplayer javascript game using a node.js server (with socket.io) and I am looking for a way to share code, specifically classes, between the web client and the server. Luckily, I came across this resource: ...

Tips for creating a recursive string literal type in Typescript

I need to create a type that represents a series of numbers separated by ':' within a string. For example: '39:4893:30423', '232', '32:39' This is what I attempted: type N = `${number}` | '' type NL = `${ ...

Images with spaces in their names are failing to load in React Native

I am currently trying to utilize an image within my react native application. At this point, my code is very minimal. The following code snippet is all I have for now: import React from 'react'; import { ScrollView, View, Text } from 'reac ...

Are there any methods for utilizing the Angular/flex-layout API within a TypeScript file in an Angular 11 project?

When working with Angular Material, the Angular Flex Layout proves to be quite beneficial. Is there a way to access the flex layout API within a TypeScript file? For instance, can we retrieve MediaQueries values from this link in a TypeScript file? breakp ...

Error messages encountered following the latest update to the subsequent project

Recently, I upgraded a Next project from version 12 to 14, and now I'm encountering numerous import errors when attempting to run the project locally. There are too many errors to list completely, but here are a few examples: Import trace for requeste ...

Interactive loadChild components

I've been attempting to dynamically import routes from a configuration file using the following code snippet: export function buildRoutes(options: any, router: Router, roles: string[]): Routes { const lazyRoutes: Routes = Object.keys(options) ...

Is it possible to observe a class instance without including it in the constructor?

Currently, I'm in the process of testing my Node TypeScript application with Jest. My goal is to avoid altering my existing class, which looks something like this: export class UserRepository { async createUser(userData: User) { const pris ...

Securing Angular 2 routes with Auth Guard through canActivate

I've been searching for a solution to this problem for the past 4 hours with no luck. I have multiple Authguards set up, and I want to instruct the router to grant permission if any of them are true, rather than requiring all guards to be true. Curre ...

Expanding Material UI functionality across various packages within a monorepository

Currently, I am using lerna to develop multiple UI packages. In my project, I am enhancing @material-ui/styles within package a by incorporating additional palette and typography definitions. Although I have successfully integrated the new types in packag ...

What is the reason for TS expressing dissatisfaction about the use of a type instead of a type entry being provided

Below is a snippet of code for a Vue3 component that takes in an Array of Objects as a prop: <script lang="ts"> interface CorveesI { What: string, Who: string, Debit: number } export default { name: 'Corvees', props: { ...

The Observable pipeline is typically void until it undergoes a series of refreshing actions

The issue with the observable$ | async; else loading; let x condition usually leads to staying in the loading state, and it requires multiple refreshes in the browser for the data to become visible. Below is the code snippet that I utilized: // TypeScript ...

leveraging parcel for importing typescript dependencies

I am currently using parcel to process typescript for a web extension. I have installed JQuery and its type definitions via npm. In my typescript file, I have the following at the top: import $ from "jquery"; import "bootstrap"; However, when running run ...

Require using .toString() method on an object during automatic conversion to a string

I'm interested in automating the process of utilizing an object's toString() method when it is implicitly converted to a string. Let's consider this example class: class Dog { name: string; constructor(name: string) { this.name = na ...

Incorporating an external module into your Angular application for local use

I currently have two projects: my-components, which is an Angular library, and my-showcase, an Angular app. Whenever I make changes or add a new component to my library, I commit it to a private git repository and publish it to a private npm registry. This ...

Incorporating HTML code within a .ts file: a basic guide

I'm relatively new to Angular, but I've been given a project that's built with Angular. As I examine the .ts file containing the list of property types, I need to wrap a span around the label text. Is this doable? Here is the current list ...

Incorporating a Component with lazy-loading capabilities into the HTML of another Component in Angular 2+

Striving to incorporate lazy loading in Angular 2, I have successfully implemented lazy loading by following this helpful guide. Within my application, I have two components - home1 and home2. Home1 showcases the top news section, while home2 is dedicated ...

Unable to load dynamic JSON data in ag-grid for Angular 2

ngOnInit(){ this.gridOptions = {}; this.gridOptions.rowData = []; this.gridOptions.rowData = [ {configName: 1, configName1: "Jarryd", configName2: "Hayne", configName3: "tttttt", configName4: "rrrtttt", configName5:"drrrrrr"}]; } ...

What techniques can be employed to dynamically modify Typescript's AST and run it while utilizing ts-node?

Below is my approach in executing a TypeScript file: npx ts-node ./tinker.ts In the file, I am reading and analyzing the Abstract Syntax Tree (AST) of another file named sample.ts, which contains the following line: console.log(123) The goal is to modify ...