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

struggle with converting JSON string into an array from server

After receiving JSON data from the server, I attempted to convert it into an array using: JSON.parse(response.data.blocks) However, I encountered this error: SyntaxError: Unexpected token o in JSON at position 1 at JSON.parse (<an ...

Learn the process of updating an Xero invoice seamlessly with the xero-Node integration

After successfully integrating Xero with my app for accounting and billing purposes, I am now looking to update a previous invoice that was created in the past on Xero. Is there a solution available for this? Any suggestions would be greatly appreciated. ...

Create a package themed with Material UI for export

I am attempting to create a new npm package that exports all @material-ui/core components with my own theme. I am currently using TypeScript and Rollup, but encountering difficulties. Here is the code I have: index.ts export { Button } from '@materia ...

I'm facing some difficulties in sourcing my header into a component and encountering errors. Can anyone advise me on how to properly outsource my header?

Looking to streamline my headers in my Ionic 4 project by creating a separate reusable component for them. Here's how I set up my dashboard page with the header component: <ion-header> <app-header title="Dashboard"></app-he ...

Using Angular expressions, you can dynamically add strings to HTML based on conditions

I currently have the following piece of code: <div>{{animalType}}</div> This outputs dog. Is there a way to conditionally add an 's' if the value of animalType is anything other than dog? I attempted the following, but it did not ...

Crafting Effective AngularJS Directives

Recently, I've been delving into AngularJS and have grasped the core concepts quite well. As I embark on building my own application, I find myself struggling with laying out the blueprint, particularly in terms of directive design. Are there any not ...

Tips for successfully passing an entire object in the data of a datatable column property

I am currently using Datatables version 1.10.21 and implementing ajax with the column property to generate the datatables successfully. ajax: { url: dataUrl, //using dataUrl variable dataSrc: 'data' }, co ...

Trigger JavaScript keypress event on keypress

In my code, I have a function called addMoney() that is triggered by pressing Enter in an input field: $('body').on('keypress', 'input[type=text]', function(e){ if(e.keyCode==13){ var val = $('input[type=te ...

A JavaScript object featuring a predefined value

I have a simple goal that I'm unsure about achieving. My objective is to create an object that will return a specific value if no property is specified. For example: console.log(obj) // Should output "123" console.log(obj.x) // Should output "ABC" ...

What is the best way to transfer variables to a different page through a pop-up window?

I'm working with a function that converts the Id of the clicked element into a variable, then opens a new window with a different page. How can I access or utilize this variable on the newly opened page? var idToWrite = []; $(function(){ $(".szl ...

Steps to Hide a Material-UI FilledInput

Trying to format a FilledInput Material-ui component to show currency using the following package: https://www.npmjs.com/package/react-currency-format Various attempts have been made, but none seem to be successful. A codesandbox showcasing the issue has ...

Incorporating Checkbox Value into Textbox with classic ASP and jQuery

Here is a glimpse of the code I've written: response.write "<th align=left><font size=2>" response.write "1. <input type=checkbox class='checkboxes' value='ORG'>Organization" response.write "</font> ...

Tips for emphasizing a searched item in V-data-table using VueJS

Is it possible to highlight a specific value in a v-data-table by searching for a particular word? Below is a snippet of my current code: EDIT: I am using the CRUD-Actions Example from the official Vuetify documentation. https://vuetifyjs.com/en/componen ...

Implementing a delay using setTimeOut function to execute an action upon user input

Take a look at this code snippet: $("#test").on("keyup", (e) => { if(e.target.value.length === 3) { setTimeout(() => { console.log("fired") }, 2000) } }) <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.m ...

Using local fonts with Styled Components and React TypeScript: A beginner's guide

Currently, I'm in the process of building a component library utilizing Reactjs, TypeScript, and Styled-components. I've come across the suggestion to use createGlobalStyle as mentioned in the documentation. However, since I am only working on a ...

show various commands and outcomes of the getElementsByClassName() function

Can someone help me figure out how to use the "getElementsByClassName() Method" in JavaScript to change or display different colors? I am trying to change the background color to blue for the class "ex" and red for the class "example", but it's not wo ...

Observing fluctuations in variable values within Angular2

How can I track changes in a variable bound to an input type text? I attempted using Observables, but the change event is not being triggered. Does anyone have an example or documentation on this? ...

How to delete an item from an object in TypeScript

Can you help with filtering an object in Angular or TypeScript to eliminate objects with empty values, such as removing objects where annualRent === null? Additionally, what method can we use to round a number like 2.833333333333335 to 2.83 and remove the ...

Discover the path with the power of JavaScript

Imagine I am including a JavaScript file in this manner: <script type="text/javascript" src="http://foo.com/script.js?id=120#foo"></script> Would it be possible to access the GET or hash parameters passed through this? Currently, I achieve t ...

extracting the ID from within an iframe

Currently, I am developing a script using pure javascript. parent.*nameofiframe*.document.getElementById('error').value; However, when attempting to achieve the same using jQuery, it doesn't seem to work: $('*nameofiframe*', win ...