Leverage generics to assign a static type to the key of a record in a way that refers back to

I am working on developing a finite state machine companion for my chatbot automation library. The aim is to guide users towards different conversation phases while interacting with the bot.

The plan is for the users of the library to supply a "state machine descriptor" to a function that will then create an instance of the state machine.

Although all functionalities are operational, I am looking to enhance the static typing for users of the library.

My goal is to achieve the following:

const myStateMachine = createStateMachine({
    initialState: "state1",
    states: {
        state1: {
            onMessage: (requester: MessageObj, stateMachine: StateMachineInstance) => {
                requester.reply("hello1");
                stateMachine.setState("state2");
            }
        },
        state2: {
            onMessage: (requester: MessageObj, stateMachine: StateMachineInstance) => {
                requester.reply("hello2");
                stateMachine.setState("state1");
            }
        }
    }
});

Presently, I am encountering an issue: the method stateMachine.setState("state2") is accepting any string, rather than the specified state keys in the descriptor. It should only allow "state1" | "state2" since those are the determined states of the state machine.

I have attempted various solutions, but most of them are resulting in TypeScript errors. As a temporary solution, I have reverted back to a generic string to ensure compilation.

These are the current type definitions:

type StateId = string;

type State =
    {
        onMessage: (
            requester: MessageObj,
            stateMachineInstance: StateMachineInstance
        ) => any;
    }

type StateMachineDescriptor = {
    initialState: StateId;
    states: {
        [stateId: string]: State,
    }
};

type StateMachineInstance = StateMachineDescriptor & {
    currentState: StateId;
    setState: (newState: StateId) => void;
    reset: () => void;
};

Answer №1

My approach to crafting a viable solution is outlined below:

To address this issue, I have integrated generics which are essential for resolution. Here is the function signature utilizing generics:

declare function createStateMachine<States extends string>(value: StateMachineDescriptor<States>): StateMachineInstance<States>;

Utilizing an object type for States may be functional, but utilizing it to represent state names provides better clarity. TypeScript faces an initial challenge in inferring States from the incorrect source. Consider the following scenario:

type StateMachineDescriptor<States extends string> = {
    initialState: States;
    states: Record<States, State<States>>;
};

In this case, TypeScript infers States from initialState rather than states, which is not the intended behavior. To alter inference on initialState, a workaround is implemented to reposition it:

type NoInfer<T> = [T][T extends any ? 0 : never];

By blocking inference at initialState, the functionality behaves as anticipated:

type StateMachineDescriptor<States extends string> = {
    initialState: NoInfer<States>;
    states: Record<States, State<States>>;
};

This rectifies the primary issue. The next step is to introduce a generic parameter:

type StateMachineInstance<States extends string> = StateMachineDescriptor<States> & {
    currentState: States;
    setState: (newState: States) => void;
    reset: () => void;
};

type State<States extends string> =
    {
        onMessage: (
            requester: MessageObj,
            stateMachineInstance: StateMachineInstance<States>
        ) => any;
    }

Interactive Playground

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

What is the process for connecting a date/time form control?

My code seems to only be working for the 'title' element, while the 'docdatetime' control remains blank. Can anyone spot what I'm doing wrong? //template =================================================== <div class="form-grou ...

Interfaces and Accessor Methods

Here is my code snippet: interface ICar { brand():string; brand(brand:string):void; } class Car implements ICar { private _brand: string; get brand():string { return this._brand; } set brand(brand:string) { this. ...

What is the most effective way to retrieve a specific type of sibling property in TypeScript?

Consider the code snippet below: const useExample = (options: { Component: React.ComponentType props: React.ComponentProps<typeof options.Component> }) => { return } const Foo = (props: {bar: string; baz: number}) => <></& ...

The validation through class-validator or class-transformer fails to function properly when applying multiple Types to the @Query decorator

Is there a way to combine multiple types with the @Query() decorator in my controller, such as ParamsWithRegex and PaginationParams? I'm facing an issue where no validation is being applied when I do that. How can I resolve this problem? **// MY CON ...

Resolving TypeScript strictNullChecks Error When Validating Nested Object Property

Encountering an issue with TypeScript's strictNullChecks setting. There is a function handleAction that requires an argument of type MyType. type MyType = { prop: MyEnum; // ... other properties }; enum MyEnum { Value1, Value2, // ... other ...

Are there any methods within Angular 2 to perform Angular binding within a string?

When creating an HTML template with routing, such as shown below: <ul class="sb-sub-menu"> <li> <a [routerLink]="['clientadd']">Client Add</a> </li> </ul> It functions as expected. However, w ...

Clicking on a single checkbox causes the entire input to become deactivated due to the way the system is

I'm encountering a puzzling issue that has me feeling like I know the solution, yet I don't. I set "State" to [checked]. The problem arises when, upon turning it into a map and clicking just one checkbox, the entire selection is clicked. To addre ...

Displaying nested objects within an object using React

Behold this interesting item: const [object, setObject] = useState ({ item1: "Greetings, World!", item2: "Salutations!", }); I aim to retrieve all the children from it. I have a snippet of code here, but for some reason, i ...

Typescript: Retrieve an interface containing properties that are found in interface A, but not in interface B

I am currently developing a mapper that will facilitate the translation between a serialized entity state and a form state. In the context of two given interfaces A and B, I am exploring ways to derive a third interface C that includes properties present ...

Automatically export as a namespace in a declaration file

I have a compact TypeScript library that is exported as UMD, and I generate the *.d.ts file automatically by setting "declaration": true in my tsconfig. The exported file contains: export class Blue { alert(): void { console.log('alerte ...

Re-evaluating information when the query parameter is modified?

While working on my angular 2 projects, I encountered an issue where I couldn't reload the page by changing the query parameters within the same URL. I am currently utilizing resolve to fetch data before loading the page. I am now striving to figure ...

Can you guide me on how to programmatically set an option in an Angular 5 Material Select dropdown (mat-select) using typescript code?

I am currently working on implementing an Angular 5 Material Data Table with two filter options. One filter is a text input, while the other is a dropdown selection to filter based on a specific field value. The dropdown is implemented using a "mat-select" ...

Creating a Dynamic Example in Scenario Outline Using Typescript and Cypress-Cucumber-Preprocessor

I have a question that is closely related to the following topic: Behave: Writing a Scenario Outline with dynamic examples. The main difference is that I am not using Python for my Gherkin scenarios. Instead, I manage them with Cypress (utilizing the cypre ...

Guide on integrating Amazon S3 within a NodeJS application

Currently, I am attempting to utilize Amazon S3 for uploading and downloading images and videos within my locally running NodeJS application. However, the abundance of code snippets and various credential management methods available online has left me fee ...

In React-Typescript, the second index of the todos array is constantly being updated while the rest of the array remains unchanged

I am struggling to get my Todo-List working with typescript-react. The code I have doesn't seem to be functioning properly. Here is a snippet of my App.tsx: import { useState } from "react"; import "./App.css"; export default fun ...

How to dynamically insert variables into a separate HTML file while creating a VS Code extension

Currently working on a vscode extension, I'm facing an issue with containing the html in a string (like this: https://github.com/microsoft/vscode-extension-samples/blob/main/webview-view-sample/src/extension.ts). It leads to a large file size and lack ...

Issues with the md-radio-button not functioning properly in Angular 2

I'm in need of two radio buttons to choose between two options. Here's the code I'm using: <md-radio-button [value]=true [(ngModel)]="carSelected" name="carOption">Car</md-radio-button> <md-radio-button [value]=false [(ngMode ...

Issue with Angular router failing to load the correct component

As a novice with Angular, I have the following routes set up. app.routing.module.ts import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { FrameComponent } from './ui/frame/frame.compon ...

Assigning initial value to a BehaviorSubject in an Angular application

I'm still getting the hang of Rxjs. How do I go about initializing the first value of a BehaviorSubject with data from a custom select box model? Here's what the model looks like: private mainRangeDate: any = {beginDate: {year: 2018, mon ...

Querying with Node SQLite fails to return a value

So, here's my little dilemma: I have 3 methods that need to access a database file (SQLite3). export function F_SetupDatabase(_logger: any): void export function Q_RunQuery(query: string, db: "session" | "global"): any export func ...