Develop customizable enumerations for use in expandable interfaces

Situation: My objective is to devise a strategy for building scalable state machines in TypeScript using the TypeState library. TypeState offers a typesafe state machine for Typescript, which while not directly related to my current issue, serves as a good example of what I am aiming for.

Challenge: I am facing difficulties in creating a flexible pattern for extending enums in TypeScript and implementing them within interface and class declarations.

Objective: The following pseudo code demonstrates the blueprint of the pattern that I want to achieve.

1) Establish a base enum called States

2) Expand on enum States by adding more states to create enum ExtendedStates

3) Define ParentInterface utilizing States and a typed state machine

4) Extend ParentInterface with ChildInterface, overriding States with ExtendedStates

5) Implement ParentInterface in class Parent

6) Enhance class Parent into class Child implementing ChildInterface

7) Ensure that broadcastState() can be called from either class to retrieve the current state.

I have successfully utilized this pattern in other programming languages, and I would appreciate guidance on understanding TypeScript limitations and possible alternative patterns to achieve the same outcome.

import {TypeState} from "typestate";

enum States {
  initialState
}

// finding an alternative to extend since it's not available on enum
enum ExtendedStates extends States {
  AdditionalState
}

/////////////////////////////////////////
// works without any issues
interface ParentInterface {
  fsm: TypeState.FiniteStateMachine<States>;
  states: typeof States;
  message: string;
}

// incorrectly extends ParentInterface, mismatching types of fsm/states
interface ChildInterface extends ParentInterface {
  fsm: TypeState.FiniteStateMachine<ExtendedStates>;
  states: typeof ExtendedStates;
}

/////////////////////////////////////////

class Parent implements ParentInterface {
  public fsm: TypeState.FiniteStateMachine<States>;
  public states: typeof States;
  public message: string = "The current state is: ";

  constructor(state: States | undefined) {
    state = state ? state : this.states.initialState;
    this.fsm = new TypeState.FiniteStateMachine(state);
    this.broadcastCurrentState();
  }

  public broadcastCurrentState(): void {
    console.log(this.message + this.fsm.currentState);
  }
}

class Child extends Parent implements ChildInterface {
  public fsm: TypeState.FiniteStateMachine<ExtendedStates>;
  public states: typeof ExtendedStates;

  constructor(state: ExtendedStates | undefined) {
    state = state ? state : this.states.initialState;
    this.fsm = new TypeState.FiniteStateMachine(ExtendedStates);
    this.broadcastCurrentState();
  }
}

Best Attempt So Far

import {TypeState} from "typestate";

enum States {
  initialState
}

enum ExtendedStates {
  initialState,
  extendedState
}

class Parent {
  public fsm: TypeState.FiniteStateMachine<States>;
  public states: typeof States;
  public message: string = "The current state is: ";

  // T is declared but never used
  constructor(state: <T> | undefined) {
    state = state ? state : this.states.initialState;
    // cannot find name T
    this.fsm = new TypeState.FiniteStateMachine<T>(state);
    this.broadcastCurrentState();
  }

  public broadcastCurrentState(): void {
    console.log(this.message + this.fsm.currentState);
  }
}

// incompatible types for fsm
class Child extends Parent {
  public fsm: TypeState.FiniteStateMachine<ExtendedStates>;
  public states: typeof ExtendedStates;

  constructor(state: ExtendedStates | undefined) {
    // Parameter not assignable to type <T>
    super(state);
  }
}

This attempt comes close to the desired outcome but fails to compile, resulting in excessive enum duplication. It also lacks interfaces, which are optional but offer additional safety measures.

I am eager to hear your insights. I believe this approach holds considerable potential, and there may be a simple solution that I am overlooking.

Answer №1

One issue causing the compilation error is that Child is not a suitable subtype of Parent. According to the Liskov substitution principle, a Child object should be usable as a Parent object. If I request the state of a state machine from a Parent object and it responds with ExtendedState, then the Parent object is flawed, correct? Therefore, a Child is an imperfect version of a Parent, which is problematic and what TypeScript tries to caution you about.

It might be more effective to discard the superclass/subclass relationship and opt for a generic class:

class Generic<T extends States> {
  public fsm: TypeState.FiniteStateMachine<T>;
  public states: T;
  public message: string = "The current state is: ";

  constructor(state: T[keyof T] | undefined) {
    state = state ? state : this.states.InitialState;
    this.fsm = new TypeState.FiniteStateMachine<T>(state);
    this.broadcastCurrentState();
  }

  public broadcastCurrentState(): void {
    console.log(this.message + this.fsm.currentState);
  }
}

This approach would function if the States were appropriate objects, but as observed, enums lack the necessary features for this usage - they cannot be extended. Rather than using an enum, consider employing an object that mimics its behavior:

// define our own enum
type Enum<T extends string> = {[K in T]: K};

// generate an enum from specified values
function makeEnum<T extends string>(...vals: T[]): Enum<T> {
  const ret = {} as Enum<T>;
  vals.forEach(k => ret[k] = k)
  return ret;
}

// expand an existing enum with additional values
function extendEnum<T extends string, U extends string>(
  firstEnum: Enum<T>, ...vals: U[]): Enum<T | U> {
    return Object.assign(makeEnum(...vals), firstEnum) as any;  
}

In this case, an Enum<> acts as an object with designated string keys, where the values match the key (unlike traditional enums, which have numerical values). If numeric values are desired, it could be implemented but may be more complex. Since I haven't used the TypeState library before, I am unsure whether it requires numeric or string values. Now, you can create your States and ExtendedStates like this:

const States = makeEnum('InitialState'); 
type States = typeof States; 

const ExtendedStates = extendEnum(States, 'ExtendedState');
type ExtendedStates = typeof ExtendedStates;

and construct objects as follows:

const parentThing = new Generic<States>(States.InitialState);
const childThing = new Generic<ExtendedStates>(ExtendedStates.InitialState);

I hope this information proves beneficial; best of luck!

Answer №2

One option is to utilize the x-extensible-enum tool. If you need further understanding, feel free to consult:

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

Establish a connection to the ActiveMQ broker utilizing STOMP protocol in an Ionic application

I've recently received an Ionic + Capacitor app that is primarily meant to run on the Android platform. My current task is to incorporate communication with a remote ActiveMQ broker into the app. To achieve this, I utilized the STOMP JS library which ...

How can you merge arrays in Angular based on their unique names?

Is there a way to combine objects within the same array in Angular based on their names? [{ "name":"Navin", "id":"1" }, { "name":"Navin", "mark1":"98" ...

Filtering an array of objects based on another array of objects in Angular2 through the use of pipes

I'm having trouble understanding how to use the angular pipe to filter an array of objects based on another array of objects. Currently, I have a pipe that filters based on a single argument. I am working with two arrays, array1 and array2, both cont ...

TypeScript and Next.js failing to properly verify function parameters/arguments

I'm currently tackling a project involving typescript & next.js, and I've run into an issue where function argument types aren't being checked as expected. Below is a snippet of code that illustrates the problem. Despite my expectation ...

What is the reason behind Typescript errors vanishing after including onchange in the code?

When using VSCode with appropriate settings enabled, the error would be displayed in the following .html file: <!DOCTYPE html> <html> <body> <div> <select> </select> </div> <script&g ...

"Encountering a Prisma type error while attempting to add a new record

I am currently utilizing Prisma with a MySQL database. Whenever I attempt to create a new record (School), an error of type pops up in the console. Additionally, I am implementing a framework called Remix.run, although it does not seem to be causing the is ...

Issue "unable to use property "useEffect", dispatcher is undefined" arises exclusively when working with a local npm package

I am currently in the process of creating my very own private npm package to streamline some components and functions that I frequently use across various React TypeScript projects. However, when I try to install the package locally using its local path, ...

How to seamlessly incorporate Polymer Web Components into a Typescript-based React application?

Struggling to implement a Polymer Web Components tooltip feature into a React App coded in TypeScript. Encountering an error during compilation: Error: Property 'paper-tooltip' does not exist on type 'JSX.IntrinsicElements' To resolve ...

Executing a function within the same file is referred to as intra-file testing

I have two functions where one calls the other and the other returns a value, but I am struggling to get the test to work effectively. When using expect(x).toHaveBeenCalledWith(someParams);, it requires a spy to be used. However, I am unsure of how to spy ...

Guide to encapsulating a container within a map function using a condition in JSX and TypeScript

Currently, I am working with an array of objects that are being processed by a .map() function. Within this process, I have a specific condition in mind - if the index of the object is greater than 1, it should be enclosed within a div element with a parti ...

What is the best way to implement debouncing for an editor value that is controlled by the parent component?

Custom Editor Component import Editor from '@monaco-editor/react'; import { useDebounce } from './useDebounce'; import { useEffect, useState } from 'react'; type Props = { code: string; onChange: (code: string) => void ...

Removing a field from a collection using firebase-admin: Tips and tricks

I currently have a collection stored in Firebase Realtime Database structured like this: My requirement is to remove the first element (the one ending with Wt6J) from the database using firebase-admin. Below is the code snippet I tried, but it didn' ...

Unable to sign out user from the server side using Next.js and Supabase

Is there a way to log out a user on the server side using Supabase as the authentication provider? I initially thought that simply calling this function would work: export const getServerSideProps: GetServerSideProps = withPageAuth({ redirectTo: &apos ...

Vue - Troubleshooting why components are not re-rendering after data updates with a method

Check out this simple vue component I created: <template> <div class="incrementor"> <p v-text="counter"></p> <button v-on:click="increment()">Increment</button> </div> </template> <script lan ...

Creating Algorithms for Generic Interfaces in TypeScript to Make them Compatible with Derived Generic Classes

Consider the (simplified) code: interface GenericInterface<T> { value: T } function genericIdentity<T>(instance : GenericInterface<T>) : GenericInterface<T> { return instance; } class GenericImplementingClass<T> implemen ...

I encountered an issue where TypeScript's generics function was unable to locate a property within an interface

I am attempting to define a function in typescript using generics, but I encountered the following error: "Property 'id' does not exist on type 'CustomerInterface'" This occurs at: customer.id === +id getCustomer<Custo ...

Changing the appearance of a specific child component in React by referencing its id

There is an interface in my code. export interface DefaultFormList { defaultFormItems?: DefaultFormItems[]; } and export interface DefaultFormItems { id: string; name: string; formXml: string, isDefaultFormEnable: boolean; } I am looking ...

Initially, when an iframe is loaded in Angular 10, it may display a 404 error page

Hey there! I'm currently using the HTML code below to incorporate an iframe and display an external page hosted on the same domain so no need to worry about cross domain issues: <iframe frameborder="0" [src]="url"></iframe ...

What is the best way to modify an object within a pure function in JavaScript?

Currently, I am exploring different strategies to ensure that a function remains pure while depending on object updates. Would creating a deep copy be the only solution? I understand that questions regarding object copying are quite common here. However, ...

The `Required<Partial<Inner>>` does not inherit from `Inner`

I stumbled upon a code snippet that looks like this: type Inner = { a: string } type Foo<I extends Inner> = { f: I } interface Bar<I extends Inner> { b: I } type O<I extends Partial<Inner>> = Foo<Required<I>> & B ...