What is the most effective approach for annotating TypeScript abstract classes that are dynamically loaded?

I am in the process of developing a library that allows for the integration of external implementations, and I am exploring the optimal approach to defining types for these implementations.

Illustration

abstract class Creature {
    public abstract makeNoises();
}

class Lion extends Creature {
    public makeNoises() {
        console.log('roar');
    }
}

class Elephant extends Creature {
    public makeNoises() {
        console.log('trumpet');
    }
}

type BuiltInCreatures = 'lion' | 'elephant';

interface CreatureLike {
    [name: string]: new () => Creature;
}
default class SafariClient {
    public starCreature: Creature;
    constructor(someCreature: BuiltInCreatures | CreatureLike) {
        if (typeof someCreature === 'string') {
            // load `Lion` for 'lion', or `Elephant` for 'elephant'.
            // this.starCreature = new Lion() or new Elephant();
        } else {
           // integrate external creature plugin
           // this.starCreature = new [someCreature]();
        }
    }

    public makeNoises() {
        this.starCreature.makeNoises();
    }
}

I intend to offer predefined classes that can be easily utilized, while also allowing users to introduce their own custom classes. How can I achieve this?

const safari = new SafariClient('lion');
// or
const safari = new SafariClient(new Giraffe()); // Or maybe `new SafariClient(Giraffe)`?

I am specifically interested in finding an elegant solution that provides clear options to users of SafariClient - the type system should indicate that they can use either a string (BuiltInCreature) or a custom implementation of Creature.

Answer №1

Just a quick note, at the moment your Cat and Dog types are identical in structure. This means that the compiler cannot differentiate between them. While this may not cause any issues, it can lead to unexpected outcomes (such as IntelliSense indicating that a Dog is actually a Cat). To avoid unintentional equivalence between types in my code examples, I prefer to do the following:

class Dog extends Animal {
  chaseCars() {}
  public makeSounds() {
    console.log("woof");
  }
}

class Cat extends Animal {
  chaseMice() {}
  public makeSounds() {
    console.log("meow");
  }
}

By structurally differentiating between a Cat and a Dog (one chases mice while the other chases cars) along with their unique names, everything falls into place.


My suggestion would be to create a registry of predefined Animal constructors:

const builtInAnimals = {
  cat: Cat,
  dog: Dog
};

And define an associated type:

type BuiltInAnimals = typeof builtInAnimals;

This allows for the implementation of your ZooClient class as follows:

class ZooClient {
  public mostFamousAnimal: Animal;
  constructor(someAnimal: keyof BuiltInAnimals | (new () => Animal)) {
    const animalConstructor =
      typeof someAnimal === "string" ? builtInAnimals[someAnimal] : someAnimal;
    this.mostFamousAnimal = new animalConstructor();
  }

  public makeSounds() {
    this.mostFamousAnimal.makeSounds();
  }
}

The constructor now accepts either a keyof BuiltInAnimals (in this case, either "cat" or "dog") or a constructor returning an Animal. The animalConstructor variable makes use of a typeof type guard to determine the type of someAnimal, ensuring it is set as new() => Animal. The constructor is then used accordingly.

Let's check its functionality:

const dogZooClient = new ZooClient("dog");
dogZooClient.makeSounds(); // woof

class Dolphin extends Animal {
  makeSounds() {
    console.log("🐬🔊");
  }
}
const dolphinZooClient = new ZooClient(Dolphin);
dolphinZooClient.makeSounds(); // 🐬🔊

As intended, the client performs as expected. Let's ensure there are no unintended uses:

new ZooClient("badName"); // error!
// Argument of type '"badName"' is not assignable to
// parameter of type '"cat" | "dog" | (new () => Animal)'.

class NotAnAnimal {
  makeSmells() {
    console.log("👃");
  }
}
new ZooClient(NotAnAnimal); // error!
// Property 'makeSounds' is missing in type 'NotAnAnimal'
// but required in type 'Animal'.

The above cases are correctly rejected.


Hope this information proves helpful. Best of luck!

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

Refreshing the page causes Material UI Button to revert to default styling

My question regarding the Material UI Button losing styling after a page refresh (link: Material UI Button loses Styling after page refresh) went unanswered, so I am reposting with a CodeSandbox included for reference: https://codesandbox.io/s/bold-resonan ...

When organizing Node.js express routes in separate files, the Express object seamlessly transforms into a Router object for efficient routing

I am currently working on a Node.js application with Express. I organize my routes using tsoa, but when I introduce swagger-ui-express to the project, an error occurs Error: TypeError: Router.use() requires a middleware function but got undefined Here is ...

Exploring the capabilities of VueJs in detecting events triggered by parent components

When users click on a specific image in the Collection(parent component), my goal is to display that image in a Modal(child component). Below is the code snippet: routes.js import Home from './components/Home'; import About from './compone ...

The functionality of $watch in AngularJS is not meeting the desired outcomes

Within my controller, I am looking to receive notifications when the value of a certain variable changes. Specifically, I want a function to be triggered whenever this variable is updated. To achieve this, I am utilizing the $watch method in AngularJS. Her ...

Exploring the capabilities of Node.js functions

I'm currently exploring Node.js and struggling to understand how functions are created and used. In my code snippet: var abc={ printFirstName:function(){ console.log("My name is abc"); console.log(this===abc); //Returns true ...

Guide to implementing the HttpOnly flag in a Node.js and Express.js application

Hey there! I'm currently working on a project using node.js and I need to ensure that the HttpOnly flag is set to true for the header response. I've written this code snippet in my app.js file but it doesn't seem to be affecting the respons ...

Why does adding to an array in the Vuex store replace all existing values with the last entry?

Utilizing vuex-typescript, here is an example of a single store module: import { getStoreAccessors } from "vuex-typescript"; import Vue from "vue"; import store from "../../store"; import { ActionContext } from "vuex"; class State { history: Array<o ...

How can Vue.js pass an array from a child component to its parent component?

I'm currently developing a contact book app and I have created a modal within a child component that contains a form with several input fields. My goal is to utilize the values entered in the form and add them to the parent component. I have successfu ...

Implement styling based on user input - Transmit form data via PHP to designated email address

My form allows people to provide their details and share their current timetable. I then group them based on suitable times The PHP form currently prints either 'work' or 'free' into a table cell, based on user selection in the form, a ...

Error message in the browser console: Uncaught TypeError - The function allSections.addEventListener is not recognized

Whenever I inspect my browser console, I keep encountering this error: Uncaught TypeError: allSections.addEventListener is not a function at PageTransitions (app.js:16:17) at app.js:33:1 I find it strange because my VS Code editor does not display any err ...

Issue with Component: Data is not being injected into controller from ui-router resolve, resulting in undefined data

Encountering an issue with resolve when using a component and ui-router: the data returned after resolving the promise is displaying as "undefined" in the controller. Service: class userService { constructor ($http, ConfigService, authService) { th ...

Upon transitioning from typescript to javascript

I attempted to clarify my confusion about TypeScript, but I'm still struggling to explain it well. From my understanding, TypeScript is a strict syntactical superset of JavaScript that enhances our code by allowing us to use different types to define ...

Perform an Ajax request with JQuery in an HTML document and transfer the response to a different HTML page

This is my first attempt at using AJAX and jQuery to retrieve data from another HTML page. Below is the code I have written: Here is my JavaScript code: <script type="text/javascript> $(document).ready(function() { $('#submit').click( ...

What is the best way to set up an on-change listener for material-ui's <CustomInput...>?

I'm currently utilizing the React dashboard created by Creative Tim. I have a question regarding how to set up an onChange listener for a Here is the code snippet for the custom input class: import React from "react"; import classNames from "classna ...

Delaying UI interactivity until the document is fully loaded

I'm currently developing a web application that must be compatible with Internet Explorer 8 (yes, you read it right, compatible with the HELL). The issue I'm facing is with uploading a file which is later processed by PHP code and then refreshes ...

Sorting through names within a nested array based on specific criteria

I have been struggling to filter by item name for the past day and haven't been successful. The issue lies in my attempt to use a sample array for filtering. While I am able to filter by category successfully, the same cannot be said for filtering by ...

Utilizing the "return" keyword in Javascript outside of function declarations

Exploring the Impact of Using the Return Keyword in JavaScript Scripts Beyond Functions in Browsers and Node.js Recently, I experimented with utilizing the return keyword in a Node.js script like so: #!/usr/bin/env node return 10; My initial assumption ...

Troubleshooting incorrect data display in AngularJS using ng-repeat

Being a newbie in the world of JavaScript, AngularJS, and Parse, I am eager to learn. If there are any mistakes in my approach, please do point them out as I aim to gain as much knowledge as possible. I have been given an assignment that requires me to ut ...

Can someone provide instructions on how to convert base64 data to an image file

I'm utilizing the vue-signature Library but I am unsure how to download the base64 data that is generated as an image. Here is the link to the library: https://www.npmjs.com/package/vue-signature. I have gone through the documentation and noticed that ...

Using PHP, create a redirect page that utilizes AJAX and jQuery for a seamless user experience

My goal is to navigate from page a to the profile page with a post session in between. Let's assume that the data is stored in a variable called $name as a string. The current code on page a looks like this: jQuery("#result").on("click",function(e){ ...