What are some ways to specialize a generic class during its creation in TypeScript?

I have a unique class method called continue(). This method takes a callback and returns the same type of value as the given callback. Here's an example:

function continue<T>(callback: () => T): T {
   // ...
}

Now, I'm creating a class that is parameterized over the result type of the continue() function which we'll call Semantic. My continue() method in this class will pass along the result to the callback:

class Checkpoint<Semantic> {
   function continue<T>(callback: (val: Semantic) => T): T {
      // ...
   }
}

This Semantic type can be one of a few possible types, and I need to differentiate behavior at runtime based on the type of Checkpoint being used:

type runtimeDiscriminator = 'Script' | 'Statement'

class Checkpoint<Semantic> {
   type: runtimeDiscriminator

   constructor(blah: any, type: runtimeDiscriminator) {
      // ...
      this.type = type
   }

   continue<T>(callback: (val: Semantic) => T): T {
      if (this.type === 'Script') { /* ... do things */ }
      else { /* ... do other things */ }
   }
}

The challenge I'm facing is trying to incorporate this information into the type-system and using user-defined type guards to ensure proper typing throughout the process.

type runtimeDiscriminator = 'Script' | 'Statement'

class Checkpoint<Semantic> {
   type: runtimeDiscriminator

   constructor(blah: any, type: 'Script'): Checkpoint<Script>
   constructor(blah: any, type: 'Statement'): Checkpoint<Statement>
   constructor(blah: any, type: runtimeDiscriminator) {
      // ...
      this.type = type
   }

   producesScript(): this is Checkpoint<Script> {
      return this.type === 'Script'
   }

   producesStatement(): this is Checkpoint<Statement> {
      return this.type === 'Statement'
   }

   continue<T>(callback: (val: Semantic) => T): T {
      // ... perform runtime checks and narrow down the resultant type from `callback`
   }
}

However, I encountered an error from the typechecker:

Type annotation cannot appear on a constructor declaration. ts(1093)

I'm puzzled by this restriction and unsure how to proceed without duplicating annotations at every callsite. How can I properly overload constructors like this?


Edit: Hopefully, I haven't mixed up TypeScript terminology too much. Coming from OCaml background, the C++-like terms in TypeScript can sometimes get confusing!

Answer №1

Regarding TypeScript naming conventions: typically, generic type parameter names consist of one or two uppercase characters, even though this may limit expressiveness. Furthermore, type aliases and interfaces often start with an initial capital letter, while non-constructor values usually start with an initial lowercase letter. These conventions will be adhered to in this context.


To proceed effectively, consider creating a mapping from discriminator RuntimeDiscriminator to discriminated type Semantic, and making your Checkpoint class generic based on the discriminator itself. Here is an example:

interface SemanticMap {
  Script: Script;
  Statement: Statement;
}

type RuntimeDiscriminator = keyof SemanticMap;

It's worth noting that you don't necessarily need a single instance of the SemanticMap interface in your code; it simply helps the type system understand the relationship between string literal names and types (interfaces are ideal for this purpose).

class Checkpoint<K extends RuntimeDiscriminator> {
  type: K;

  constructor(blah: any, type: K) {
    this.type = type;
  }

  producesScript(): this is Checkpoint<"Script"> {
    return this.type === "Script";
  }

  producesStatement(): this is Checkpoint<"Statement"> {
    return this.type === "Statement";
  }

In referencing your Semantic type, utilize the lookup type SemanticMap[K], as shown in the signature of the continue() method:

 continue<T>(callback: (val: SemanticMap[K]) => T): T {
    return callback(getSemanticInstance(this.type)); // or something
  }

}

(In implementing continue(), you may find yourself needing to use type assertions or similar approaches since the compiler generally struggles with assigning concrete values to generics due to safety concerns; see microsoft/TypeScript#24085. This limitation applies across the board, not just when using SemanticMap[K] instead of Semantic.)

Let's confirm that it functions as expected:

function scriptAcceptor(s: Script): string {
  return "yummy script";
}

function statementAcceptor(s: Statement): string {
  return "mmmm statement";
}

const scriptCheckpoint = new Checkpoint(12345, "Script"); // Checkpoint<"Script">
const scrVal = scriptCheckpoint.continue(scriptAcceptor); // string

const statementCheckpoint = new Checkpoint(67890, "Statement"); // Checkpoint<"Statement">
const staVal = statementCheckpoint.continue(statementAcceptor); // string

const oops = scriptCheckpoint.continue(statementAcceptor); // error!
//                                     ~~~~~~~~~~~~~~~~~~~~
// Argument of type '(s: Statement) => string' is not assignable
// to parameter of type '(val: Script) => string'.

Everything seems to be functioning correctly.


As a side note, if you opt to implement the continue() method by calling those type guards and switching on the result, you might want to consider transforming Checkpoint<K> into an abstract superclass and introducing concrete subclasses like

ScriptCheckpoint extends Checkpoint<"Script">
and
StatementCheckpoint extends Checkpoint<"Statement">
, each with its own implementation of continue(). This approach can offload the responsibility from Checkpoint of having to fulfill multiple roles. However, without understanding your specific scenario, I won't elaborate further on this idea; it's just something to keep in mind.


Link to 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

EventManager gathering of various subscriptions

Is it possible for JhiEventManager to handle multiple subscriptions, or do I need a separate subscription for each event? Will the destroy() method of JhiEventManager handle multiple subscriptions as well? export class SomeComponent implements OnInit, OnDe ...

Creating a specialized TypeScript interface by extending a generic one

Create a customized interface using TypeScript that inherits from a generic interface by excluding the first parameter from all functions. Starting with the following generic interface: interface GenericRepository { insertOne<E>(entity: Type<E& ...

React's .map is not compatible with arrays of objects

I want to retrieve all products from my API. Here is the code snippet for fetching products from the API. The following code snippet is functioning properly: const heh = () => { products.map((p) => console.log(p.id)) } The issue ari ...

Building a Dynamic Video Element in Next Js Using TypeScript

Is there a way to generate the video element in Next JS using TypeScript on-the-fly? When I attempt to create the video element with React.createElement('video'), it only returns a type of HTMLElement. However, I need it to be of type HTMLVideoEl ...

Is it possible to inject a descendant component's ancestor of the same type using

When working with Angular's dependency injection, it is possible to inject any ancestor component. For example: @Component({ ... }) export class MyComponent { constructor(_parent: AppComponent) {} } However, in my particular scenario, I am tryin ...

Extracting an array from an HTTP response in Angular/Typescript using the map operator or retrieving a specific element

Q1: How can I extract an array of objects from a http response using map in Angular? Q2: Is there a way to retrieve a specific object from a http response by utilizing map in Angular? Below are the example URL, sample data, CurrencyAPIResponse, and Curre ...

Issue with border radius in MUI 5 affecting table body and footer elements

Currently, I am diving into a new project utilizing React version 18.2 and MUI 5.10.3 library. My main task involves designing a table with specific styles within one of the components. The table header should not display any border lines. The table body ...

What is the reason behind installing both Typescript and Javascript in Next.js?

After executing the command npx create-next-app --typescript --example with-tailwindcss my_project, my project ends up having this appearance: https://i.stack.imgur.com/yXEFK.png Is there a way to set up Next.js with Typescript and Tailwind CSS without i ...

Retrieving PHP information in an ionic 3 user interface

We are experiencing a problem with displaying data in Ionic 3, as the data is being sourced from PHP REST. Here is my Angular code for retrieving the data: this.auth.displayinformation(this.customer_info.cid).subscribe(takecusinfo => { if(takecusi ...

What could be the reason for my Angular 2 app initializing twice?

Can someone help me figure out why my app is running the AppComponent code twice? I have a total of 5 files: main.ts: import { bootstrap } from '@angular/platform-browser-dynamic'; import { enableProdMode } from '@angular/core'; impor ...

The namespace does not contain any exported member

Every time I attempt to code this in TypeScript, an error pops up stating The namespace Bar does not have a member named Qux exported. What could possibly be causing this and how can I resolve it? class Foo {} namespace Bar { export const Qux = Foo ...

Retrieve the value of the Observable when it is true, or else display a message

In one of my templates, I have the following code snippet: <app-name val="{{ (observable$ | async)?.field > 0 || "No field" }}" The goal here is to retrieve the value of the property "field" from the Observable only if it is grea ...

The 'eventKey' argument does not match the 'string' parameter. This is because 'null' cannot be assigned to type 'string'

Encountering an issue while invoking a TypeScript function within a React Project. const handleLanguageChange = React.useCallback((eventKey: eventKey) => { setLanguage(eventKey); if(eventKey == "DE") setCurre ...

Having difficulty launching a TypeScript Node.js application using ts-node and pm2

I have a node app built with TypeScript and it works fine with nodemon, but when I try to move it to the server using PM2, I encounter errors. I've searched GitHub and StackOverflow for solutions, but nothing has worked for me so far. Any help would b ...

Ensuring User Authentication in Angular with Firebase: How to Dynamically Hide the Login Link in Navigation Based on User's Login Status

After successfully implementing Angular Firebase email and password registration and login, the next step is to check the user's state in the navigation template. If the user is logged in, I want to hide the login button and register button. I tried s ...

If "return object[value1][value2] || default" does not work, it means that value1 is not a recognized property

Within my code, there is an object literal containing a method that retrieves a sub-property based on a specific input. If the lookup fails, it should return a default property. //private class, no export class TemplateSelection { 'bills'; & ...

Creating secure RSA keys using a predetermined seed - a step-by-step guide

Is it possible to utilize a unique set of words as a seed in order to recover a lost private key, similar to how cryptocurrency wallets function? This method can be particularly beneficial for end-to-end encryption among clients, where keys are generated o ...

Updating from React 17 to React 18 in Typescript? The Children of ReactNode type no longer supports inline conditional rendering of `void`

When using the React.ReactNode type for children, inline conditional renders can cause failures. Currently, I am utilizing SWR to fetch data which is resulting in an error message like this: Type 'false | void | Element | undefined' is not assig ...

The Recoil Nexus encountered an error: the element type provided is not valid. It should be a string for built-in components or a class/function for composite components, but an object was

Encountered the following error message: Error - Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object. at ReactDOMServerRenderer.render ... This issue arose when integra ...

The 'RouterLink' JSX element type is missing any construct or call signatures

While researching this issue on the internet and Stack Overflow, I've noticed a common theme with problems related to React. An example can be found here. However, I am working with Vue and encountering errors in Vue's own components within a new ...