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

It’s not possible for Typescript to reach an exported function in a different module

Having trouble referencing and using exported methods from another module. I keep getting an error that says 'There is no exported member in SecondModule'. module FirstModule{ export class someClass{ constructor(method: SecondModule ...

How can I toggle the visibility of a div after the DOM has finished loading?

I was experimenting with a radio button on the interface linked to a property in the typescript file that controls the visibility of another div. However, I noticed that both *ngIf="isFooSelected" and [hidden]="!isFooSelected" only function upon initial pa ...

Chess.js TypeScript declaration file for easy implementation

Currently, I am delving into learning typescript and have taken up the challenge of crafting a declaration file for the chess.js library. However, it seems that I am struggling to grasp the concept of creating one. Whenever I attempt to import the library ...

What is the best way to upload a file using a relative path in Playwright with TypeScript?

Trying to figure out how to upload a file in a TypeScript test using Playwright. const fileWithPath = './abc.jpg'; const [fileChooser] = await Promise.all([ page.waitForEvent('filechooser'), page.getByRole('button' ...

At what point do we employ providers within Angular 2?

In the Angular 2 documentation, they provide examples that also use HTTP for communication. import { HTTP_PROVIDERS } from '@angular/http'; import { HeroService } from './hero.service'; @Component({ selector: 'my-toh&ap ...

Check to see whether the coordinates fall inside the specified bounding box

I am faced with the task of creating a function that can determine whether a given coordinate c lies within the boundaries of coordinates a and b. All variables in this scenario are of type: type Coordinate = { lat: number; lon: number; }; Initially ...

Create a dynamically updating list using React's TypeScript rendering at regular intervals

My goal is to create a game where objects fall from the top of the screen, and when clicked, they disappear and increase the score. However, I am facing an issue where the items are not visible on the screen. I have implemented the use of setInterval to d ...

Trigger parent Component property change from Child Component in Angular2 (akin to $emit in AngularJS)

As I delve into teaching myself Angular2, I've encountered a practical issue that would have been easy to solve with AngularJS. Currently, I'm scouring examples to find a solution with Angular2. Within my top-level component named App, there&apos ...

I'm struggling to figure out the age calculation in my Angular 2 code. Every time I try to run it,

Can anyone assist me with calculating age from a given date in Angular? I have written the following code but I keep getting undefined in the expected field. This is Component 1 import { Component, OnInit, Input } from '@angular/core'; ...

Prisma auto-generating types that were not declared in my code

When working with a many-to-many relationship between Post and Upload in Prisma, I encountered an issue where Prisma was assigning the type 'never' to upload.posts. This prevented me from querying the relationship I needed. It seems unclear why P ...

The specified property 'XYZ' is not found in the type 'Readonly<{ children?: ReactNode; }> & Readonly<{}>'

Whenever I try to access .props in RecipeList.js and Recipe.js, a syntax error occurs. Below is the code snippet for Recipe.js: import React, {Component} from 'react'; import "./Recipe.css"; class Recipe extends Component { // pr ...

What is preventing me from running UNIT Tests in VSCode when I have both 2 windows and 2 different projects open simultaneously?

I have taken on a new project that involves working with existing unit tests. While I recently completed a course on Angular testing, I am still struggling to make the tests run smoothly. To aid in my task, I created a project filled with basic examples f ...

Angular default route with parameters

Is it possible to set a default route with parameters in Angular, such as www.test.com/landing-page?uid=123&mode=front&sid=de8d4 const routes: Routes = [ { path: '', redirectTo: 'landing-page', pathMatch: 'full' }, ...

There was an error reported by TypeScript stating that Nest.js is not considered a module

During the development of my project using Nest.js, I came across an error that I couldn't find a solution for. The issue arises when I try to export a function from one file and import it into a controller. Even though the function is exported correc ...

Error message: Unable to access property 'post' from undefined - Angular 2

Here is the snippet of code in my component file: import { Component, Injectable, Inject, OnInit, OnDestroy, EventEmitter, Output } from '@angular/core'; import { Http, Response, Headers, RequestOptions } from '@angular/http'; import & ...

Error: Reference to an undeclared variable cannot be accessed. TypeScript, Cordova, iOS platforms

Can anyone offer some advice on what might be the issue? I'm encountering an error while building my Ionic app on the IOS platform, but everything is running smoothly on Android. ReferenceError: Cannot access uninitialized variable. service.ts:31 O ...

What is the purpose of specifying the props type when providing a generic type to a React functional component?

When utilizing the @typescript-eslint/typedef rule to enforce type definitions on parameters, I encountered an issue with generically typing a React.FC: export const Address: React.FunctionComponent<Props> = (props) => ( An error was thrown st ...

Adding a condition to the react-router v6 element: A step-by-step guide

I am currently in the process of updating my project from v5 to v6 of react-router-dom. However, I have encountered an issue. Everything was working fine in v5 <Route path={`${url}/phases/:phaseIndex`}> {(chosenPhase?.type === PhaseTy ...

Error: Callstack Overflow encountered in TypeScript application

Here is the code snippet that triggers a Callstack Size Exceeded Error: declare var createjs:any; import {Animation} from '../animation'; import {Events} from 'ionic-angular'; import { Inject } from '@angular/core'; exp ...

TypeScript: Extending a Generic Type with a Class

Although it may seem generic, I am eager to create a class that inherits all props and prototypes from a generic type in this way: class CustomExtend<T> extends T { constructor(data: T) { // finding a workaround to distinguish these two ...