Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Autocomplete for service.send() #232

Merged
merged 4 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/late-crabs-begin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"robot3": minor
---

Autocomplete for service.send()

This makes it so that the event name in `service.send(event)` is inferred from the transitions used to create the machine.
32 changes: 24 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
],
"devDependencies": {
"@changesets/cli": "^2.26.0",
"node-qunit-puppeteer": "^2.2.0",
"bundlesize": "^1.0.0-beta.2",
"local-web-server": "^4.2.1",
"node-qunit-puppeteer": "^2.2.0",
"wireit": "^0.9.3"
},
"wireit": {
Expand All @@ -31,5 +31,8 @@
}
}
}
},
"dependencies": {
"expect-type": "^1.1.0"
}
}
91 changes: 62 additions & 29 deletions packages/core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,23 @@ declare module 'robot3' {
}[keyof T]
: never

type AllStateKeys<T> = NestedKeys<T> | keyof T
type AllStateKeys<T> = NestedKeys<T> | keyof T;

type MachineStates<S = {}, F extends string = string> = {
[K in keyof S]: {
final: boolean
transitions: Map<string, Transition<F>[]>
immediates?: Map<string, Immediate<F>[]>
enter?: any
}
}

/**
* The debugging object contains an _onEnter method, wich can be set to invoke
* this function on every transition.
*/
export const d: {
_onEnter?: OnEnterFunction<Machine>
_onEnter?: OnEnterFunction<Machine<any>>
}

/**
Expand All @@ -27,29 +36,31 @@ declare module 'robot3' {
* @param states - An object of states, where each key is a state name, and the values are one of *state* or *invoke*.
* @param context - A function that returns an object of extended state values. The function can receive an `event` argument.
*/
export function createMachine<S = {}, C = {}>(
export function createMachine<S extends MachineStates<S, F>, C = {}, F extends string = string>(
initial: keyof S,
states: { [K in keyof S]: MachineState },
states: S,
context?: ContextFunction<C>
): Machine<typeof states, C, AllStateKeys<S>>
): Machine<S, C, AllStateKeys<S>>
/**
* The `createMachine` function creates a state machine. It takes an object of *states* with the key being the state name.
* The value is usually *state* but might also be *invoke*.
*
* @param states - An object of states, where each key is a state name, and the values are one of *state* or *invoke*.
* @param context - A function that returns an object of extended state values. The function can receive an `event` argument.
*/
export function createMachine<S = {}, C = {}>(
states: { [K in keyof S]: MachineState },
export function createMachine<S extends MachineStates<S, F>, C = {}, F extends string = string>(
states: S,
context?: ContextFunction<C>
): Machine<typeof states, C, AllStateKeys<S>>
): Machine<S, C, AllStateKeys<S>>;

/**
* The `state` function returns a state object. A state can take transitions and immediates as arguments.
*
* @param args - Any argument needs to be of type Transition or Immediate.
*/
export function state(...args: (Transition | Immediate)[]): MachineState
export function state<T extends Transition<any> | Immediate<any>>(
...args: T[]
): MachineState<T extends Transition<infer F> ? F : string>;

/**
* A `transition` function is used to move from one state to another.
Expand All @@ -58,11 +69,11 @@ declare module 'robot3' {
* @param state - The name of the destination state.
* @param args - Any extra argument will be evaluated to check if they are one of Reducer, Guard or Action.
*/
export function transition<C, E>(
event: string,
export function transition<F extends string, C, E>(
event: F,
state: string,
...args: (Reducer<C, E> | Guard<C, E> | Action<C, E>)[]
): Transition
): Transition<F>;

/**
* An `immediate` function is a type of transition that occurs immediately; it doesn't wait for an event to proceed.
Expand All @@ -71,10 +82,10 @@ declare module 'robot3' {
* @param state - The name of the destination state.
* @param args - Any extra argument will be evaluated to check if they are a Reducer or a Guard.
*/
export function immediate<C, E>(
export function immediate<F extends string, C, E>(
state: string,
...args: (Reducer<C, E> | Guard<C, E> | Action<C, E>)[]
): Transition
): Transition<F>

/**
* A `guard` is a method that determines if a transition can proceed.
Expand Down Expand Up @@ -119,23 +130,23 @@ declare module 'robot3' {
* @param fn - Promise-returning function
* @param args - Any argument needs to be of type Transition or Immediate.
*/
export function invoke<C, T, E extends {} = any>(fn: (ctx: C, e?: E) => Promise<T>, ...args: (Transition | Immediate)[]): MachineState
export function invoke<C, T, E extends {} = any>(fn: (ctx: C, e?: E) => Promise<T>, ...args: (Transition<any> | Immediate<any>)[]): MachineState<any>

/**
* The `invoke` is a special type of state that immediately invokes a Promise-returning or Machine-returning function, or another machine.
*
* @param fn - Machine-returning function
* @param args - Any argument needs to be of type Transition or Immediate.
*/
export function invoke<C, E extends {} = any, M extends Machine>(fn: (ctx: C, e?: E) => M, ...args: (Transition | Immediate)[]): MachineState
export function invoke<C, E extends {} = any, M extends Machine = any>(fn: (ctx: C, e?: E) => M, ...args: (Transition<any> | Immediate<any>)[]): MachineState<any>

/**
* The `invoke` is a special type of state that immediately invokes a Promise-returning or Machine-returning function, or another machine.
*
* @param machine - Machine
* @param args - Any argument needs to be of type Transition or Immediate.
*/
export function invoke<M extends Machine>(machine: M, ...args: (Transition | Immediate)[]): MachineState
export function invoke<M extends Machine>(machine: M, ...args: (Transition<any> | Immediate<any>)[]): MachineState<any>

/* General Types */

Expand All @@ -151,8 +162,8 @@ declare module 'robot3' {
service: Service<T>
) => void

export type SendEvent = string | { type: string; [key: string]: any }
export type SendFunction<T = SendEvent> = (event: T) => void
export type SendEvent<T extends string = string> = T | { type: T; [key: string]: any }
export type SendFunction<T extends string> = (event: SendEvent<T> & {}) => void

/**
* This function is invoked before entering a new state and is bound to the debug
Expand All @@ -164,16 +175,16 @@ declare module 'robot3' {
* @param prevState - previous state
* @param event - event provoking the state change
*/
export type OnEnterFunction<M extends Machine> =
export type OnEnterFunction<M extends Machine<any>> =
<C = M['state']>(machine: M, to: string, state: C, prevState: C, event?: SendEvent) => void

export type Machine<S = {}, C = {}, K = string> = {
export type Machine<S extends MachineStates<S, F> = {}, C = {}, K = string, F extends string = string> = {
context: C
current: K
states: S
state: {
name: K
value: MachineState
value: MachineState<F>
}
}

Expand All @@ -189,15 +200,15 @@ declare module 'robot3' {
fn: GuardFunction<C, E>
}

export interface MachineState {
export interface MachineState<F extends string> {
final: boolean
transitions: Map<string, Transition[]>
immediates?: Map<string, Immediate[]>
transitions: Map<F, Transition<F>[]>
immediates?: Map<F, Immediate<F>[]>
enter?: any
}

export interface Transition {
from: string | null
export interface Transition<F extends string> {
from: F | null
to: string
guards: any[]
reducers: any[]
Expand All @@ -208,8 +219,30 @@ declare module 'robot3' {
machine: M
context: M['context']
onChange: InterpretOnChangeFunction<M>
send: SendFunction
send: SendFunction<GetMachineTransitions<M>>
}

export type Immediate<F extends string> = Transition<F>;

// Utilities
type IsAny<T> = 0 extends (1 & T) ? true : false;

// Get state objects from a Machine
type GetMachineStateObject<M extends Machine> = M['states'];

// Create mapped type without the final indexing
type GetTransitionsFromStates<S> = {
[K in keyof S]: S[K] extends { transitions: Map<string, Array<Transition<infer F>>> }
? IsAny<F> extends true
? never
: F
: never
}

export type Immediate = Transition
type ExtractNonAnyValues<T> = {
[K in keyof T]: IsAny<T[K]> extends true ? never : T[K]
}[keyof T] & {};

export type GetMachineTransitions<M extends Machine> =
ExtractNonAnyValues<GetTransitionsFromStates<GetMachineStateObject<M>>>;
}
12 changes: 12 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
"bundlesize": "wireit",
"server": "wireit",
"test": "wireit",
"test:types": "wireit",
"test:browser": "wireit",
"build:cjs": "wireit",
"build": "wireit"
},
Expand Down Expand Up @@ -74,6 +76,16 @@
}
},
"test": {
"dependencies": [
"test:types",
"test:browser"
]
},
"test:types": {
"command": "tsc -p test/types/tsconfig.json",
"files": []
},
"test:browser": {
"command": "node-qunit-puppeteer http://localhost:1965/test/test.html 10000",
"dependencies": [
"server"
Expand Down
24 changes: 24 additions & 0 deletions packages/core/test/types/send.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { expectTypeOf } from 'expect-type';
import { test } from 'node:test';
import {
type Service,
createMachine,
transition,
state,
} from 'robot3';

test('send(event) is typed', () => {
const machine = createMachine({
one: state(transition('go-two', 'two')),
two: state(transition('go-one', 'one')),
three: state()
});

type Params = Parameters<Service<typeof machine>['send']>;
type EventParam = Params[0];
type StringParams = Extract<EventParam, string>;
expectTypeOf<StringParams>().toEqualTypeOf<'go-one' | 'go-two'>();

type ObjectParams = Extract<EventParam, { type: string; }>;
expectTypeOf<ObjectParams['type']>().toEqualTypeOf<'go-one' | 'go-two'>();
});
Loading