Exploring Generics and Type Inference within TypeScript

(In reference to this question)

I've developed a function that takes in a configuration object containing state definitions, essentially creating a state machine.

For example:

const machine = createStateMachine({
  initial: 'inactive',
  states: stateNode({
    inactive: {
      on: { ACTIVATE: 'active' },
    },
    active: stateNode({
      on: { DEACTIVATE: 'inactive' },
      effect(send) {
        send('DEACTIVATE') // Only accepts "DEACTIVATE".
      },
    }),
  }),
});

The user can transition the current state by using the send method, which is accessible in two ways:

  • Returned from the effect function within the configuration
  • Returned from the createStateMachine function

I aim to deduce as much information as possible from the configuration, and the stateNode serves as an intermediate function aiding in type inference:

function stateNode<T extends Record<keyof T, PropertyKey>>({on, effect}: {
  on: T, effect?: (send: (action: keyof T) => void) => void}) {
  return { on, ...effect && { effect } }
}

Everything runs smoothly so far, but I am exploring ways to make the "on" property optional - any suggestions?

Feel free to check out the full code on TS Playground

Answer №1

Essentially, by making the on property optional throughout your code and utilizing the NonNullable<T> utility type wherever the type of on is used programmatically, everything should work seamlessly:

type States<T extends Record<keyof T, { on?: any }>> = { [K in keyof T]: {
  on?: Record<keyof (NonNullable<T[K]['on']>), keyof T>,
  effect?(send: (action: keyof NonNullable<T[K]['on']>) => void): void;
} }

function createStateMachine<
  T extends States<T>>(states: {
    initial: keyof T,
    states: T
  }) {
  return function send(arg: KeysOfTransition<NonNullable<T[keyof T]['on']>>) {
  };
}

function stateNode<T extends Record<keyof T, PropertyKey>>({ on, effect }: {
  on?: T, effect?: (send: (action: keyof T) => void) => void
}) {
  return { ...on && { on }, ...effect && { effect } }
}

However, there's a slight hiccup with this approach:

const a = createStateMachine({
  initial: 'inactive',
  states: {
    inactive: stateNode({
      on: {
        ACTIVATE: 'active'
      }
    }),
    frozen: stateNode({ effect() { console.log("Entered Frozen") } }), // error!
//  ~~~~~~
// Type 'Record<string | number | symbol, string | number | symbol>' is not assignable to 
// type 'Record<string | number | symbol, "inactive" | "active" | "frozen">'.
    active: stateNode({
      on: {
        DEACTIVATE: 'inactive',
      },
      effect: send => {
        send('DEACTIVATE')
      },
    })
  },
});
// const a: (arg: string | number | symbol) => void

The issue arises from T being inferred incorrectly when the on property is omitted. This results in undesirable behavior where the compiler forgets state names and outputs less useful types like (arg: PropertyKey) => void. To address this, we can refactor the stateNode() function into an overloaded function for better control:

function stateNode<T extends Record<keyof T, PropertyKey>>(param: {
  on: T;
  effect?: ((send: (action: keyof T) => void) => void) | undefined;
}): typeof param;
function stateNode(param: { effect?: () => void, on?: undefined }): typeof param;
function stateNode<T extends Record<keyof T, PropertyKey>>({ on, effect }: {
  on?: T, effect?: (send: (action: keyof T) => void) => void
}) {
  return { ...on && { on }, ...effect && { effect } }
}

With these changes, specifying or omitting the on property in the stateNode() calls will yield consistent and desired outcomes as demonstrated below:

const a = createStateMachine({
  initial: 'inactive',
  states: {
    inactive: stateNode({
      on: {
        ACTIVATE: 'active'
      }
    }),
    frozen: stateNode({ effect() { console.log("Entered Frozen") } }),
    active: stateNode({
      on: {
        DEACTIVATE: 'inactive',
      },
      effect: send => {
        send('DEACTIVATE')
      },
    })
  },
});
// const a: (arg: "ACTIVATE" | "DEACTIVATE") => void

All seems to be working smoothly now!

https://www.typescriptlang.org/play?ts=4.2.3#code/C4TwDgpgBA0hIGcDyAzAKgJwIYDsEEth8B7HAHiQCMArAPigF4orqoIAPYCHAEwSgBKEAMbEMPMgGt4xFMxoAaKAmAZ8OAOb0A-FGkhZ81gC4oOCADcIGANwBYAFCPQkKAGVgWLgjJo2nbj5BETEJfUM0JQBvKFJtU1wQKABfWnomGIBtGCh1PRk5NABdUyjHK...

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

Apollo-Server presents errors in a polished manner

It seems like the question explains itself adequately. I am currently using 'apollo-server-core' version 3.6.5 Desired Errors: { "errors": [ { "message": "Syntax Error: Unexpected < ...

The concept of callback function overloading using generic types in TypeScript

Is there a way to define a callback type in TypeScript that can accept a variable number of generic type arguments while keeping the number of arguments fixed? For instance: export interface CustomFn { <T1>(value1: T1): boolean <T1,T2>(va ...

What is the outcome when the return type is a string or a function that takes validation arguments and returns a string?

This excerpt is extracted from line 107 over here. From my understanding, it indicates: The function either returns a string directly or a function that accepts ValidationArguments as input and then produces a string output. Given that this is new to m ...

What could be the reason for mocha failing to function properly in a project that is set up

During a unit test in my TypeScript project using mocha, I encountered an issue when setting the project type to module. The error message displayed is as follows: ➜ typescript-project yarn test yarn run v1.22.17 warning package.json: No license field $ ...

Hiding columns in TanStack Ver 8: A step-by-step guide

Can someone assist me with dynamically hiding specific columns in Tanstack table ver 8? I have a column defined as follows: { header: "Case Number", accessorKey: "caseNumber", visible: false, // using this value ...

Sending a style prop to a React component

My typescript Next.js app seems to be misbehaving, or perhaps I'm just not understanding something properly. I have a component called <CluckHUD props="styles.Moon" /> that is meant to pass the theme as a CSS classname in order to c ...

Is it possible to execute a script from a different directory?

Currently, I am developing a nodejs package that involves executing a bash script within the program. The specific bash script, "./helpers/script.sh", needs to be run using a relative path. This means that when users install and run the package, the script ...

Implement the Promise return type for a function in Angular

I'm looking to modify the following method to change the return type to a Promise or Observable: basketItemNodes: TreeNode[] = []; getBasketItems() { this.basketService.getAllBasketItems() .subscribe( res => { this.basketItemN ...

What is the process for passing a URL from a component and assigning it as the new service URL?

Hello, I am new to Angular and I am trying to pass a URL from a component and set it as the new service URL. Here is my code: pokemon.service.ts private _url: string = "https://pokeapi.co/api/v2/pokemon"; constructor(private http : HttpClient) { } ...

Leveraging the Cache-Control header in react-query for optimal data caching

Is it possible for the react-query library to consider the Cache-Control header sent by the server? I am interested in dynamically setting the staleTime based on server instructions regarding cache duration. While reviewing the documentation, I didn&apos ...

Nest JS is currently experiencing difficulties with extending multiple classes to include columns from other entities

Currently, I am immersed in a new project that requires me to enhance my entity class by integrating common columns from another class called BASEMODEL. import { Index, PrimaryGeneratedColumn } from "typeorm"; export class BaseModel { @Prima ...

What is the proper method of exiting a subscription within a function in Angular?

Is the following method the correct way to return a value? private canView(permission: string, resource: string): boolean { let result: boolean; this.accessChecker.isGranted(permission, resource) .pipe( take(1) ) .subsc ...

Unable to employ a custom Typescript .d.ts file

Currently, I am delving into learning TypeScript and encountering a hurdle while attempting to define a class in a TypeScript definition file and then utilize it in a TypeScript file. The dilemma lies with a JavaScript "class" called "Facade," which serve ...

TS2339 Error: The object 'Navigator' does not contain the property 'clipboard'

In the project I'm working on, there is an error that comes up when trying to copy custom data to the clipboard. It's something I can easily put together. Error TS2339: Property 'clipboard' does not exist on type 'Navigator' ...

How can you load an HTML page in Puppeteer without any CSS, JS, fonts, or images?

My current task involves using Puppeteer to scrape data from multiple pages in a short amount of time. However, upon closer inspection, I realized that the process is not as efficient as I would like it to be. This is because I am only interested in spec ...

Encountering dependency issues with minified directives that remain unresolved

My directive is functioning correctly when the application is not minified However, when I minify it, an error occurs Unknown provider: tProvider <- t <- dateTimeFilterDirective Is there a way to ensure the directive works even when minified? mod ...

Is it possible in Typescript to assign a type to a variable using a constant declaration?

My desired outcome (This does not conform to TS rules) : const type1 = "number"; let myVariable1 : typeof<type1> = 12; let type2 = "string" as const; let myVariable2 : typeof<type2> = "foo"; Is it possible to impl ...

Injector in Angular is a tool used for dependency injection

I have multiple components; I am using Injector in the constructor for encapsulation import { Component, Injector, OnInit } from '@angular/core'; @Component({ selector: 'app-base', templateUrl: './base.component.html', ...

Pressing the confirm button will close the sweet alert dialog

When pressing the confirm button, the swal closes. Is this the intended behavior? If so, how can I incorporate the loading example from the examples? Here is my swal code: <swal #saveSwal title="Are you sure?" text ="Do you want to save changes" cancel ...

How to fix an unresolved TypeScript import?

Within my node_modules directory, there is a package that contains the following import statement: import { x } from 'node_modules/@types/openfin/_v2/main'; Unfortunately, I am receiving an error message stating "cannot find module 'node_mo ...