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

Challenges faced when using an array of objects interface in Typescript

I have initialized an array named state in my component's componentDidMount lifecycle hook as shown below: state{ array:[{a:0,b:0},{a:1,b:1},{a:2,b:2}] } However, whenever I try to access it, I encounter the following error message: Prop ...

How to turn off automatic password suggestions in Chrome and Firefox

Currently, I have integrated a 'change password' feature which includes fields for 'old password', 'new password', and 'retype password'. However, the autocomplete feature is suggesting passwords from other user acco ...

What steps should be taken to trigger an API call once 3 characters have been entered into a field

In my current project, I am dealing with a parent and child component setup. The child component includes an input field that will emit the user-entered value to the parent component using the following syntax: <parent-component (sendInputValue)="g ...

How can a component access its own template specified in the @Component decorator?

@Component({ selector: 'base-comp', template: '<div>hello</div>' <-- how to get this }) export class BaseComponent { someMethod( ) { <-- referenced here } Is there a way to access the entire template i ...

What is the Correct Way to Send Functions to Custom Directives in Angular 2 Using TypeScript?

I am relatively new to Angular 2. I am currently in the process of upgrading my application from AngularJS and focusing on completing the UI/UX development. There is one final issue that I am seeking help with, and I appreciate any assistance provided. Cu ...

Executing Promises in TypeScript Sequentially

I have a collection of doc objects and I need to send an API request for each doc id in the list, executing the requests in a sequential manner. In my Typescript code, I am using Promise.all and Promise.allSelected to achieve this. [ { "doc_id&q ...

Unable to locate module 'fs'

Hey there, I'm encountering an issue where the simplest Typescript Node.js setup isn't working for me. The error message I'm getting is TS2307: Cannot find module 'fs'. You can check out the question on Stack Overflow here. I&apos ...

Using TypeScript: creating functions without defining an interface

Can function props be used without an interface? I have a function with the following properties: from - HTML Element to - HTML Element coords - Array [2, 2] export const adjustElements = ({ from, to, coords }) => { let to_rect = to.getBoundingC ...

What steps can I take to resolve a dependency update causing issues in my code?

My program stopped working after updating one of the dependencies and kept throwing the same error. Usually, when I run 'ng serve' in my project everything works fine, but after updating Chartist, I encountered this error: An unhandled exception ...

Mastering the art of correctly utilizing splice and slice

I'm having trouble identifying the issue in my code. Despite reading numerous articles on slice and splice, I am unable to achieve the desired outcome in my Angular project (not using both methods simultaneously). The results are not as expected. Belo ...

Establishing the value of "document.cookie"

Encountering issues while trying to set a cookie using different methods: Method 1: document.cookie = name + "=" + value + "; expires=" + date.toUTCString() + "; path=/"; This method only sets the value up to "name=value" wh ...

Guidance on specifying a type based on an enum in Javascript

I have a list of animals in an enum that I want to use to declare specific types. For instance: enum Animals { CAT = 'cat', DOG = 'dog', } Based on this Animal enum, I wish to declare a type structure like so: type AnimalType = { ...

It appears that TypeScript is generating incorrect 'this' code without giving any warning

I seem to be facing some resistance filing a feature request related to this on GitHub issues, so I'll give it a shot here. Here is the code snippet that caused me trouble: export class Example { readonly myOtherElement: HTMLElement; public ...

How can you incorporate a module for typings without including it in the final webpack bundle?

As I venture into the realm of Webpack, I am faced with the challenge of transitioning from TypeScript 1.x to TypeScript 2. In my previous projects, I typically worked with TypeScript in one module using separate files, TSD for typings, and compiling throu ...

Struggling to iterate through JSON data in Office Scripts?

My task involves parsing JSON data in Office Scripts to extract the headings and row details on a spreadsheet. While I have successfully fetched the data, I am encountering an error message stating that my information is not iterable at the "for" loop. ...

The breakpoint was overlooked due to the absence of generated code for TypeScript on a Windows operating system

Currently, I am in the process of debugging a TypeScript project. The structure of the project folder and tsconfig.json file is illustrated below: Furthermore, my launch.json file is displayed below: While attempting to debug, I have noticed that .map fi ...

What is the proper way to specify the type for the iterable response of Promise.all()?

It's common knowledge that Promise.all will return settled promises in the same order of the requested iterable. I'm currently grappling with how to correctly define types for individual settled resolves. I am utilizing Axios for handling asynch ...

Using Angular 4 to transfer data from a dynamic modal to a component

Currently implementing material design, I have set up a dialogService for dynamically loading MdDialog. My goal is to create a search dialog with filters that, upon submission, directs the user to a search-results component route. However, I am struggling ...

ability to reach the sub-element dictionaries in typescript

class ProvinciaComponent extends CatalogoGenerico implements OnInit, AfterViewInit { page: Page = new Page({sort: {field: 'description', dir: 'asc'}}); dataSource: ProvinciaDataSource; columns = ['codprovi ...

The variable is accessed prior to being assigned with the use of the hasOwnProperty method

Continuing my journey from JavaScript to TypeScript, I find myself faced with a code that used to function perfectly but is now causing issues. Despite searching for alternative solutions or different approaches, I am unable to resolve the problem. Snippe ...