Utilize specific class designations to establish characteristics within a particular type

Imagine I have various event classes:

class UserLoggedIn { ... }
class UserLoggedOut { ... }

Then there exists a class dedicated to handling these events:

class UserEventsManager extends AnotherClass {
    onUserLoggedIn(event) { ... }
    onUserLoggedOut(event) { ... }
}

Is there a way in TypeScript to structure such types? Can I define necessary properties and methods based on class names? My goal is this:

type UserActions = UserLoggedIn | UserLoggedOut; // ?
type ActionsHandler<TActions> = ?

// Error at compile-time: 'onUserLoggedOut' must be implemented
class UserEventsManager extends AnotherClass implements ActionsHandler<UserActions> {
    onUserLoggedIn(event) { ... }
    /* onUserLoggedOut(event) { ... } */
}

Definitely, template literals are crucial for this scenario. However, the missing element in TypeScript seems to be the ability to use class names for defining property requirements. It would be great if someone could prove me wrong about this.

Answer №1

Although it is technically possible, I would not recommend using the class name for that specific purpose. To achieve this, you would require access to the type information of UserCreated.constructor.name. However, the type of that information will always resolve to string rather than "UserCreated". As a result, you would need to implement some unconventional types to wrap your class in order to change this behavior. Even then, there is no guarantee that it would work seamlessly based on your runtime environment. Below is an example demonstrating how you could attain your desired functionality without relying on class names and instead utilizing a type string. You can further customize this approach to better suit your requirements. The key concept enabling this solution lies in the use of template literal types:

interface EventInterface {
    readonly type: string;
}

class UserCreated implements EventInterface {
    // With type being readonly, UserCreated.type will be set as "UserCreated" instead of a generic string
    type = "UserCreated";
}

type EventHandler<T extends EventInterface> = {
    // Template literal types are required when concatenating strings within types
    [K in `${"on"}${T["type"]}`]: (event: T) => void;
}

// Success!
class UserEventHandler implements EventHandler<UserCreated> {
   public onUserCreated(event: UserCreated) {

   }
}
// Unsuccessful attempt
class UserEventHandlerError implements EventHandler<UserCreated> {
   public wrongFunction(event: UserCreated) {

   }
}

Update: This also includes an illustration involving multiple events simultaneously playground

One important remark regarding UserCreated.constructor.name: If you intend to utilize this in actual code implementation rather than solely for typing purposes, I would highly advise against it. Using UserCreated.constructor.name during runtime to determine the name of the event-listener function may lead to unexpected outcomes in production settings. The name derived from UserCreated.constructor.name is considered debug data, which minifiers do not preserve. After minification, your class might appear as something like class a_ {...}, causing UserCreated.constructor.name to return a_ as the name. Hence, it is much safer to rely on explicit strings instead. While you could disable function name minification, doing so may significantly increase your bundle size.

Answer №2

Solution

// transform an event name into a handler name
type AddOn<T extends string> = `on${Capitalize<T>}`
// convert a handler name to an event name
type RemoveOn<T extends string> = T extends `on${infer E}` ? Uncapitalize<E> : T;

// generate a map of event handlers from a map of events organized by name
type EventsHandler<EventMap> = {
    [K in AddOn<Extract<keyof EventMap, string>>]: 
        RemoveOn<K> extends keyof EventMap ? (event: EventMap[RemoveOn<K>]) => void : never;
}

Explanation

@Micro S. provided you with a great answer using a name-based solution. Another approach you can consider is utilizing a map-centric strategy.

In the DOM library, numerous functions require matching an event name such as "click" to a specific event type like MouseEvent. Notably, the event types are not distinct for every event; for instance, a "click" event encompasses the same attributes as a "mousedown" event.

If we examine how these relationships are managed in lib.dom.ts, it's evident that they define these connections through maps. There exist many maps (67 to be precise), often extending one another. Each map describes the events linked to a particular object or interface. For example, there exists an interface DocumentEventMap for a Document which extends the universal events interface GlobalEventHandlersEventMap and also extends interface DocumentAndElementEventHandlersEventMap, thereby incorporating events shared by documents and HTML elements to prevent redundancy.

Instead of creating a union:

type UserEvents = UserCreated | UserVerified;

We opt for defining a mapping:

interface UserEvents {
    userCreated: UserCreated;
    userVerified: UserVerified;
}

We can leverage Typescript's template literal types coupled with mapped types to craft your EventsHandler type.

We can utilize generic template literals to convert a key "userCreated" to a method name "onUserCreated," and vice versa.

type AddOn<T extends string> = `on${Capitalize<T>}`
type RemoveOn<T extends string> = T extends `on${infer E}` ? Uncapitalize<E> : T;

type A = AddOn<'userCreated'> // type: "onUserCreated"
type B = RemoveOn<'onUserCreated'> // type: "userCreated"

The keys in our EventsHandler should be in the AddOn form. By applying the AddOn type to a union of strings, we obtain a union of their mapped versions. This necessitates the keys to be strings, so we employ Extract<keyof EventMap, string> for this purpose.

This becomes our key type:

type C = AddOn<Extract<keyof UserEvents, string>> // type: "onUserCreated" | "onUserVerified"

If our mapped interface employs the AddOn version as keys, then we ought to revert to the original keys using RemoveOn. Essentially, RemoveOn<K> should represent a key in the EventMap derived from K, albeit Typescript requires validation via extends.

type EventsHandler<EventMap> = {
    [K in AddOn<Extract<keyof EventMap, string>>]: RemoveOn<K> extends keyof EventMap ? (event: EventMap[RemoveOn<K>]) => void : never;
}

Applying this type to our UserEvents interface yields the desired outcome:

type D = EventsHandler<UserEvents>

results in:

type D = {
    onUserCreated: (event: UserCreated) => void;
    onUserVerified: (event: UserVerified) => void;
}

Consequently, we encounter the anticipated error:

Class 'UserEventsHandler' does not correctly implement interface 'EventsHandler'.

Property 'onUserVerified' is absent in type 'UserEventsHandler' but obligatory in type 'EventsHandler'.

Regrettably, you still need to specify the type for the event variable in your methods. Errors may emerge if you assign an invalid type to your event. Nonetheless, requiring only a subset of the event (e.g., onUserVerified(event: {})) or no event at all (e.g., onUserVerified()) works effectively when invoked with a UserVerified event.

Note that I retained the names from your illustration, yet the event names in the map need not align with the event class name. This scenario is acceptable too:

class UserEvent {
}

interface UserEventsMap {
    created: UserEvent;
    verified: UserEvent;
}

class UserEventsHandler2 implements EventsHandler<UserEventsMap> {
    onCreated(event: UserEvent) { }
    onVerified(event: UserEvent) {  }
}

Typescript 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

Adjusting the settimeout delay time during its execution

Is there a way to adjust the setTimeout delay time while it is already running? I tried using debounceTime() as an alternative, but I would like to modify the existing delay time instead of creating a new one. In the code snippet provided, the delay is se ...

What is the best way to store this value in a variable and incorporate it into a conditional statement within a React application?

Seeking assistance in resolving this issue. I am trying to extract the values between the backticks (``) from the span tag and store them in a variable for the logic shown below: ${research.data.codesRelated[1].code} === undefined ? ${research.data.desc ...

Absent 'dist' folder in Aurelia VS2015 TypeScript project structure

After downloading the Aurelia VS2015 skeleton for typescript, I encountered an issue trying to run the Aurelia Navigation app in IIS Express. One modification that was made to the skeleton was adding "webroot": "wwwroot" to the top level of project.json. ...

Issue: The code is throwing an error "TypeError: Cannot read property 'push' of undefined" in the JavaScript engine "Hermes

Can anyone assist me with filtering an array of objects in a TypeScript React Native project using state to store array values and filter objects in the array? Having trouble with the following error in the mentioned method: LOG after item LOG inside ...

The pathway specified is untraceable by the gulp system

Hey there, I've encountered an issue with my project that uses gulp. The gulpfile.js suddenly stopped working without any changes made to it. The output I'm getting is: cmd.exe /c gulp --tasks-simple The system cannot find the path specified. ...

Unused Angular conditional provider found in final production bundle

Looking for a way to dynamically replace a service with a mock service based on an environment variable? I've been using the ?-operator in the provider section of my module like this: @NgModule({ imports: [ ... ], providers: [ ... en ...

What steps do I need to follow in order to set the language of the npx bootstrap datepicker to pt

I have been working on a project using Angular 4, where I incorporated ngx bootstrap for date picker functionality. However, I encountered an issue with the date picker being displayed in English. In an attempt to rectify this, I added the following lines ...

Unable to update markers on agm-map for dynamic display

In my Angular 5 application, I am utilizing Angular Google Maps (https://angular-maps.com/) along with socket.io for real-time data updates of latitude and longitude from the server. Although I am successfully pushing the updated data to an array in the co ...

Restify with TypeScript:

My problem lies with a Node.js + Restify application that is written in TypeScript. I am attempting to load the Crypto module from here: import * as crypto from "crypto"; Upon compiling the script, I encounter the following error: error TS2307: Cannot f ...

Embracing Interfaces Over 'any' Types in TypeScript

https://i.stack.imgur.com/W6NMa.pngWould it be beneficial to utilize an interface as a variable type rather than opting for any? For instance, if I have 3 functions where I am declaring variables that can contain alphanumeric data, would defining them us ...

Tips for integrating Typescript Definition files with Visual Studio 2017

I have a challenge with my ASP.NET Core 2.0 application where I am attempting to incorporate TypeScript and jQuery. While TypeScript integration has been successful, I am facing issues with jQuery as it does not provide me with intellisense. Despite trying ...

"Struggling to Get Angular2 HttpClient to Properly Map to Interface

Currently, I am integrating Angular with an ASP.NET WebApi. My goal is to transmit an object from the API to Angular and associate it with an interface that I have defined in Typescript. Let me show you my TypeScript interface: export interface IUser { ...

Looking for guidance on positioning elements in Next.js when using loading.tsx? I'm trying to figure out how to center it on my screen

This is how I added suspense to my website in the past Below is the code for 'loading.tsx' ---- import React from 'react' const loading = () => { return ( <div> <span className="loading load ...

The attribute 'randomUUID' is not found within the 'Crypto' interface

I attempted to utilize the crypto.randomUUID function in my Angular app version 13.1, however, it does not seem to be accessible. When trying to use it, I encountered this error: TS2339: Property 'randomUUID' does not exist on type 'Crypto ...

Creating or accessing maps using TypeScript and Cloud Functions in Firebase

In my document, there is a map referred to as "map" with a specific structure: -document -map {id: number} {id2: number2} When the function first executes, only the document exists and I need to create the map with an initial entry. Before ...

What is the best way to obtain an error as an object when subscribing to an HTTP GET request

I am working on an asp.net core webApi along with an Angular9 WebApp. My goal is to retrieve the error in a subscribe as an object rather than just a string. this.http.post<TestSystem>(this.url, testsystem).subscribe((result) => { // do someth ...

How can you generate a distinct id value for each element within an ngFor iteration in Angular 4?

I encountered an issue where I must assign a distinct id value to each data row within my *ngFor loop in angular 4. Here is the code snippet: <div *ngFor="let row of myDataList"> <div id="thisNeedsToBeUnique">{{ row.myValue }}</div> & ...

The parameter 'users | undefined' is not compatible with the argument type 'users'. Please note that 'undefined' cannot be assigned to type 'users'

Can anyone please help me with creating a put method using TYPEORM? I am facing an error that requests a typeof, but my users have a value. (I am a newbie, so not sure). Any guidance on how to fix this issue would be greatly appreciated. Here is the snip ...

Using Typescript to import functions

TLDR - I need help understanding the difference between these imports in ReactJs using Typescript: setState1: (numbers: number[]) => void, setState2: Function Hello everyone, I've encountered some strange behavior when importing functions with Typ ...

The function "useLocation" can only be utilized within the scope of a <RouterProvider> in react-router. An Uncaught Error is thrown when trying to use useLocation() outside of a <Router>

When attempting to utilize the useLocation hook in my component, I encountered an error: import React, { useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import { connect } from 'react-redux'; import { ...