Skip to content

Commit 4f6fb69

Browse files
authored
Autocomplete for service.send() (#232)
* Add autocomplete for service.send() This makes it so you get proper auto-complete when using `service.send('event')`. * small cleanup * Add changeset * Add tests
1 parent 910dfaa commit 4f6fb69

File tree

7 files changed

+166
-38
lines changed

7 files changed

+166
-38
lines changed

.changeset/late-crabs-begin.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"robot3": minor
3+
---
4+
5+
Autocomplete for service.send()
6+
7+
This makes it so that the event name in `service.send(event)` is inferred from the transitions used to create the machine.

package-lock.json

+24-8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717
],
1818
"devDependencies": {
1919
"@changesets/cli": "^2.26.0",
20-
"node-qunit-puppeteer": "^2.2.0",
2120
"bundlesize": "^1.0.0-beta.2",
2221
"local-web-server": "^4.2.1",
22+
"node-qunit-puppeteer": "^2.2.0",
2323
"wireit": "^0.9.3"
2424
},
2525
"wireit": {
@@ -31,5 +31,8 @@
3131
}
3232
}
3333
}
34+
},
35+
"dependencies": {
36+
"expect-type": "^1.1.0"
3437
}
3538
}

packages/core/index.d.ts

+62-29
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,23 @@ declare module 'robot3' {
99
}[keyof T]
1010
: never
1111

12-
type AllStateKeys<T> = NestedKeys<T> | keyof T
12+
type AllStateKeys<T> = NestedKeys<T> | keyof T;
13+
14+
type MachineStates<S = {}, F extends string = string> = {
15+
[K in keyof S]: {
16+
final: boolean
17+
transitions: Map<string, Transition<F>[]>
18+
immediates?: Map<string, Immediate<F>[]>
19+
enter?: any
20+
}
21+
}
1322

1423
/**
1524
* The debugging object contains an _onEnter method, wich can be set to invoke
1625
* this function on every transition.
1726
*/
1827
export const d: {
19-
_onEnter?: OnEnterFunction<Machine>
28+
_onEnter?: OnEnterFunction<Machine<any>>
2029
}
2130

2231
/**
@@ -27,29 +36,31 @@ declare module 'robot3' {
2736
* @param states - An object of states, where each key is a state name, and the values are one of *state* or *invoke*.
2837
* @param context - A function that returns an object of extended state values. The function can receive an `event` argument.
2938
*/
30-
export function createMachine<S = {}, C = {}>(
39+
export function createMachine<S extends MachineStates<S, F>, C = {}, F extends string = string>(
3140
initial: keyof S,
32-
states: { [K in keyof S]: MachineState },
41+
states: S,
3342
context?: ContextFunction<C>
34-
): Machine<typeof states, C, AllStateKeys<S>>
43+
): Machine<S, C, AllStateKeys<S>>
3544
/**
3645
* The `createMachine` function creates a state machine. It takes an object of *states* with the key being the state name.
3746
* The value is usually *state* but might also be *invoke*.
3847
*
3948
* @param states - An object of states, where each key is a state name, and the values are one of *state* or *invoke*.
4049
* @param context - A function that returns an object of extended state values. The function can receive an `event` argument.
4150
*/
42-
export function createMachine<S = {}, C = {}>(
43-
states: { [K in keyof S]: MachineState },
51+
export function createMachine<S extends MachineStates<S, F>, C = {}, F extends string = string>(
52+
states: S,
4453
context?: ContextFunction<C>
45-
): Machine<typeof states, C, AllStateKeys<S>>
54+
): Machine<S, C, AllStateKeys<S>>;
4655

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

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

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

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

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

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

140151
/* General Types */
141152

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

154-
export type SendEvent = string | { type: string; [key: string]: any }
155-
export type SendFunction<T = SendEvent> = (event: T) => void
165+
export type SendEvent<T extends string = string> = T | { type: T; [key: string]: any }
166+
export type SendFunction<T extends string> = (event: SendEvent<T> & {}) => void
156167

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

170-
export type Machine<S = {}, C = {}, K = string> = {
181+
export type Machine<S extends MachineStates<S, F> = {}, C = {}, K = string, F extends string = string> = {
171182
context: C
172183
current: K
173184
states: S
174185
state: {
175186
name: K
176-
value: MachineState
187+
value: MachineState<F>
177188
}
178189
}
179190

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

192-
export interface MachineState {
203+
export interface MachineState<F extends string> {
193204
final: boolean
194-
transitions: Map<string, Transition[]>
195-
immediates?: Map<string, Immediate[]>
205+
transitions: Map<F, Transition<F>[]>
206+
immediates?: Map<F, Immediate<F>[]>
196207
enter?: any
197208
}
198209

199-
export interface Transition {
200-
from: string | null
210+
export interface Transition<F extends string> {
211+
from: F | null
201212
to: string
202213
guards: any[]
203214
reducers: any[]
@@ -208,8 +219,30 @@ declare module 'robot3' {
208219
machine: M
209220
context: M['context']
210221
onChange: InterpretOnChangeFunction<M>
211-
send: SendFunction
222+
send: SendFunction<GetMachineTransitions<M>>
223+
}
224+
225+
export type Immediate<F extends string> = Transition<F>;
226+
227+
// Utilities
228+
type IsAny<T> = 0 extends (1 & T) ? true : false;
229+
230+
// Get state objects from a Machine
231+
type GetMachineStateObject<M extends Machine> = M['states'];
232+
233+
// Create mapped type without the final indexing
234+
type GetTransitionsFromStates<S> = {
235+
[K in keyof S]: S[K] extends { transitions: Map<string, Array<Transition<infer F>>> }
236+
? IsAny<F> extends true
237+
? never
238+
: F
239+
: never
212240
}
213241

214-
export type Immediate = Transition
242+
type ExtractNonAnyValues<T> = {
243+
[K in keyof T]: IsAny<T[K]> extends true ? never : T[K]
244+
}[keyof T] & {};
245+
246+
export type GetMachineTransitions<M extends Machine> =
247+
ExtractNonAnyValues<GetTransitionsFromStates<GetMachineStateObject<M>>>;
215248
}

packages/core/package.json

+12
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
"bundlesize": "wireit",
3030
"server": "wireit",
3131
"test": "wireit",
32+
"test:types": "wireit",
33+
"test:browser": "wireit",
3234
"build:cjs": "wireit",
3335
"build": "wireit"
3436
},
@@ -74,6 +76,16 @@
7476
}
7577
},
7678
"test": {
79+
"dependencies": [
80+
"test:types",
81+
"test:browser"
82+
]
83+
},
84+
"test:types": {
85+
"command": "tsc -p test/types/tsconfig.json",
86+
"files": []
87+
},
88+
"test:browser": {
7789
"command": "node-qunit-puppeteer http://localhost:1965/test/test.html 10000",
7890
"dependencies": [
7991
"server"

packages/core/test/types/send.ts

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { expectTypeOf } from 'expect-type';
2+
import { test } from 'node:test';
3+
import {
4+
type Service,
5+
createMachine,
6+
transition,
7+
state,
8+
} from 'robot3';
9+
10+
test('send(event) is typed', () => {
11+
const machine = createMachine({
12+
one: state(transition('go-two', 'two')),
13+
two: state(transition('go-one', 'one')),
14+
three: state()
15+
});
16+
17+
type Params = Parameters<Service<typeof machine>['send']>;
18+
type EventParam = Params[0];
19+
type StringParams = Extract<EventParam, string>;
20+
expectTypeOf<StringParams>().toEqualTypeOf<'go-one' | 'go-two'>();
21+
22+
type ObjectParams = Extract<EventParam, { type: string; }>;
23+
expectTypeOf<ObjectParams['type']>().toEqualTypeOf<'go-one' | 'go-two'>();
24+
});

0 commit comments

Comments
 (0)