@doeixd/machine 0.0.17 → 0.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -60,6 +60,21 @@ export type AsyncTransitionArgs<M extends AsyncMachine<any, any>, K extends keyo
60
60
  ? A extends [...infer Rest, TransitionOptions] ? Rest : A
61
61
  : never;
62
62
 
63
+ /**
64
+ * A helper type to define a distinct state in a state machine (a "typestate").
65
+ * Allows defining the context and transitions in a single generic type.
66
+ * @template C - The context specific to this state.
67
+ * @template T - The transitions available in this state.
68
+ */
69
+ export type TypeState<C extends object, T extends object = {}> = Machine<C, T>;
70
+
71
+ /**
72
+ * A helper type to define a distinct async state in a state machine.
73
+ * @template C - The context specific to this state.
74
+ * @template T - The transitions available in this state.
75
+ */
76
+ export type AsyncTypeState<C extends object, T extends object = {}> = AsyncMachine<C, T>;
77
+
63
78
 
64
79
  /**
65
80
  * Options passed to async transition functions, including cancellation support.
@@ -330,9 +345,9 @@ export type BindTransitions<T> = {
330
345
  * @param factory - A function that receives a `transition` helper and returns the transitions object.
331
346
  * @returns A new machine instance.
332
347
  */
333
- export function createMachine<C extends object, T extends Record<string, (this: C, ...args: any[]) => any>>(
348
+ export function createMachine<C extends object, T extends Record<string, (this: C, ...args: any[]) => any> = Record<string, (this: C, ...args: any[]) => any>>(
334
349
  context: C,
335
- factory: (transition: (newContext: C) => Machine<C, T>) => T
350
+ factory: (transition: (newContext: C) => Machine<C, any>) => T
336
351
  ): Machine<C, BindTransitions<T>>;
337
352
 
338
353
  /**
@@ -569,6 +584,41 @@ export function setContext<M extends Machine<any>>(
569
584
  return createMachine(newContext, transitions as any) as M;
570
585
  }
571
586
 
587
+ /**
588
+ * Creates a minimal machine-like object with just a context property.
589
+ * Useful for creating test fixtures and working with pattern matching utilities.
590
+ *
591
+ * @template C - The context type
592
+ * @param context - The context object
593
+ * @returns An object with a readonly context property
594
+ *
595
+ * @example
596
+ * ```typescript
597
+ * // For testing with discriminated unions
598
+ * type FetchContext =
599
+ * | { status: 'idle' }
600
+ * | { status: 'success'; data: string };
601
+ *
602
+ * const idleMachine = createContext<FetchContext>({ status: 'idle' });
603
+ * const successMachine = createContext<FetchContext>({ status: 'success', data: 'result' });
604
+ *
605
+ * // Works with pattern matching
606
+ * const match = createMatcher(
607
+ * discriminantCase('idle', 'status', 'idle'),
608
+ * discriminantCase('success', 'status', 'success')
609
+ * );
610
+ *
611
+ * if (match.is.success(successMachine)) {
612
+ * console.log(successMachine.context.data); // TypeScript knows data exists
613
+ * }
614
+ * ```
615
+ */
616
+ export function createContext<C extends object>(
617
+ context: C
618
+ ): { readonly context: C } {
619
+ return { context };
620
+ }
621
+
572
622
  /**
573
623
  * Creates a new machine by overriding or adding transition functions to an existing machine.
574
624
  * Ideal for mocking in tests or decorating functionality. The original machine is unchanged.
@@ -749,7 +799,8 @@ export function matchMachine<
749
799
 
750
800
  /**
751
801
  * Type-safe helper to assert that a machine's context has a specific discriminant value.
752
- * This narrows the type of the context based on the discriminant.
802
+ * This narrows the type of the context based on the discriminant, properly handling
803
+ * discriminated unions.
753
804
  *
754
805
  * @template M - The machine type.
755
806
  * @template K - The discriminant key.
@@ -760,8 +811,12 @@ export function matchMachine<
760
811
  * @returns True if the discriminant matches, with type narrowing.
761
812
  *
762
813
  * @example
763
- * if (hasState(machine, 'status', 'loading')) {
764
- * // machine.context.status is narrowed to 'loading'
814
+ * type Context = { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: string };
815
+ * const machine = createMachine<Context>({ status: 'success', data: 'test' }, {});
816
+ *
817
+ * if (hasState(machine, 'status', 'success')) {
818
+ * // machine.context is narrowed to { status: 'success'; data: string }
819
+ * console.log(machine.context.data); // ✓ TypeScript knows about 'data'
765
820
  * }
766
821
  */
767
822
  export function hasState<
@@ -772,7 +827,7 @@ export function hasState<
772
827
  machine: M,
773
828
  key: K,
774
829
  value: V
775
- ): machine is M & { context: Context<M> & { [P in K]: V } } {
830
+ ): machine is M & { context: Extract<Context<M>, { [P in K]: V }> } {
776
831
  return machine.context[key] === value;
777
832
  }
778
833
 
@@ -1028,16 +1083,19 @@ export {
1028
1083
 
1029
1084
  export type {
1030
1085
  MachineConfig,
1031
- ExtractionConfig
1086
+ ExtractionConfig,
1087
+ ParallelRegionConfig,
1088
+ ChildStatesConfig
1032
1089
  } from './extract';
1033
1090
 
1091
+ // Note: Extraction functions (extractMachine, extractMachines, generateChart) are NOT exported
1092
+ // to keep them out of the runtime bundle. Use the CLI tool or import directly from the source
1093
+ // file for build-time statechart generation.
1034
1094
 
1035
1095
  export * from './multi'
1036
1096
 
1037
1097
  export * from './higher-order'
1038
1098
 
1039
- export * from './extract'
1040
-
1041
1099
  // =============================================================================
1042
1100
  // SECTION: MIDDLEWARE & INTERCEPTION
1043
1101
  // =============================================================================
@@ -1069,4 +1127,25 @@ export {
1069
1127
  createTransitionExtender,
1070
1128
  createFunctionalMachine,
1071
1129
  state
1072
- } from './functional-combinators';
1130
+ } from './functional-combinators';
1131
+
1132
+ // =============================================================================
1133
+ // SECTION: PATTERN MATCHING
1134
+ // =============================================================================
1135
+
1136
+ export {
1137
+ createMatcher,
1138
+ classCase,
1139
+ discriminantCase,
1140
+ customCase,
1141
+ forContext,
1142
+ type MatcherCase,
1143
+ type CasesToMapping,
1144
+ type MatcherUnion,
1145
+ type CaseNames,
1146
+ type CaseHandler,
1147
+ type ExhaustivenessMarker,
1148
+ type IsExhaustive,
1149
+ type WhenBuilder,
1150
+ type Matcher
1151
+ } from './matcher';
package/src/matcher.ts ADDED
@@ -0,0 +1,544 @@
1
+ /**
2
+ * @file Advanced Pattern Matching for State Machines
3
+ * @description
4
+ * Provides type-safe pattern matching utilities for discriminating between machine types.
5
+ * Supports three complementary APIs: type guards, exhaustive pattern matching, and simple matching.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * // Define a matcher for class-based machines
10
+ * const match = createMatcher(
11
+ * classCase('idle', IdleMachine),
12
+ * classCase('loading', LoadingMachine),
13
+ * classCase('success', SuccessMachine)
14
+ * );
15
+ *
16
+ * // API 1: Type Guards
17
+ * if (match.is.loading(machine)) {
18
+ * // machine is narrowed to LoadingMachine
19
+ * }
20
+ *
21
+ * // API 2: Exhaustive Pattern Matching
22
+ * const result = match.when(machine).is<string>(
23
+ * match.case.idle(() => 'idle'),
24
+ * match.case.loading(() => 'loading'),
25
+ * match.case.success(m => m.context.data),
26
+ * match.exhaustive
27
+ * );
28
+ *
29
+ * // API 3: Simple Match
30
+ * const name = match(machine); // 'idle' | 'loading' | 'success' | null
31
+ * ```
32
+ */
33
+
34
+ import type { Machine, Context } from './index';
35
+ import { hasState } from './index';
36
+
37
+ // =============================================================================
38
+ // SECTION: CORE TYPES
39
+ // =============================================================================
40
+
41
+ /**
42
+ * A matcher case tuple that defines a state pattern.
43
+ *
44
+ * @template Name - The unique name for this case (used for type guards and pattern matching)
45
+ * @template M - The machine type this case matches
46
+ * @template Pred - The predicate function that determines if a machine matches this case
47
+ */
48
+ export type MatcherCase<
49
+ Name extends string,
50
+ M,
51
+ Pred extends (m: any) => m is M
52
+ > = readonly [
53
+ name: Name,
54
+ machineType: M,
55
+ predicate: Pred
56
+ ];
57
+
58
+ /**
59
+ * Extracts the machine type from a MatcherCase.
60
+ */
61
+ type CaseToMachine<C> = C extends MatcherCase<any, infer M, any> ? M : never;
62
+
63
+ /**
64
+ * Extracts the case name from a MatcherCase.
65
+ */
66
+ type CaseToName<C> = C extends MatcherCase<infer Name, any, any> ? Name : never;
67
+
68
+ /**
69
+ * Builds a mapping from case names to their machine types.
70
+ */
71
+ export type CasesToMapping<Cases extends readonly MatcherCase<any, any, any>[]> = {
72
+ [C in Cases[number] as CaseToName<C>]: CaseToMachine<C>;
73
+ };
74
+
75
+ /**
76
+ * Creates a union of all possible machine types from the cases.
77
+ */
78
+ export type MatcherUnion<Cases extends readonly MatcherCase<any, any, any>[]> =
79
+ Cases[number] extends MatcherCase<any, infer M, any> ? M : never;
80
+
81
+ /**
82
+ * Extracts the union of all case names.
83
+ */
84
+ export type CaseNames<Cases extends readonly MatcherCase<any, any, any>[]> =
85
+ CaseToName<Cases[number]>;
86
+
87
+ /**
88
+ * A branded type representing a case handler in pattern matching.
89
+ * This is used internally to track which cases have been handled.
90
+ */
91
+ export type CaseHandler<Name extends string, M, R> = {
92
+ readonly __brand: 'CaseHandler';
93
+ readonly __name: Name;
94
+ readonly __machine: M;
95
+ readonly __return: R;
96
+ readonly handler: (machine: M) => R;
97
+ };
98
+
99
+ /**
100
+ * Exhaustiveness marker - signals that all cases must be handled.
101
+ */
102
+ export type ExhaustivenessMarker = {
103
+ readonly __exhaustive: true;
104
+ };
105
+
106
+ /**
107
+ * Extracts machine types from an array of case handlers.
108
+ */
109
+ type HandledMachines<Handlers extends readonly any[]> =
110
+ Handlers extends readonly [infer H, ...infer Rest]
111
+ ? (H extends CaseHandler<any, infer M, any> ? M : never) | HandledMachines<Rest>
112
+ : never;
113
+
114
+ /**
115
+ * Checks if all machine types in Union have been handled.
116
+ * Returns true if exhaustive, otherwise returns an error type with missing cases.
117
+ */
118
+ export type IsExhaustive<Union, Handled> =
119
+ Exclude<Union, Handled> extends never
120
+ ? true
121
+ : {
122
+ readonly __error: 'Non-exhaustive match - missing cases';
123
+ readonly __missing: Exclude<Union, Handled>;
124
+ };
125
+
126
+ /**
127
+ * Pattern matching builder returned by matcher.when().
128
+ */
129
+ export interface WhenBuilder<
130
+ _Cases extends readonly MatcherCase<any, any, any>[],
131
+ M
132
+ > {
133
+ /**
134
+ * Execute pattern matching with exhaustiveness checking.
135
+ *
136
+ * @template R - The return type of all handlers
137
+ * @param handlers - Array of case handlers followed by exhaustiveness marker
138
+ * @returns The result of the matched handler, or compile error if not exhaustive
139
+ *
140
+ * @example
141
+ * ```typescript
142
+ * match.when(machine).is<string>(
143
+ * match.case.idle(() => 'idle'),
144
+ * match.case.loading(() => 'loading'),
145
+ * match.exhaustive
146
+ * );
147
+ * ```
148
+ */
149
+ is<R>(
150
+ ...handlers: [...any[], ExhaustivenessMarker]
151
+ ): IsExhaustive<M, HandledMachines<typeof handlers>> extends true
152
+ ? R
153
+ : IsExhaustive<M, HandledMachines<typeof handlers>>;
154
+ }
155
+
156
+ /**
157
+ * The main Matcher interface with three APIs.
158
+ */
159
+ export interface Matcher<Cases extends readonly MatcherCase<any, any, any>[]> {
160
+ /**
161
+ * API 1: Type guard access via dynamic properties.
162
+ *
163
+ * @example
164
+ * ```typescript
165
+ * if (match.is.loading(machine)) {
166
+ * // machine is narrowed to LoadingMachine
167
+ * }
168
+ * ```
169
+ */
170
+ readonly is: {
171
+ [Name in CaseNames<Cases>]: <M>(
172
+ machine: M
173
+ ) => machine is Extract<M, CasesToMapping<Cases>[Name]>;
174
+ };
175
+
176
+ /**
177
+ * API 2a: Pattern matching builder.
178
+ *
179
+ * @example
180
+ * ```typescript
181
+ * match.when(machine).is<string>(
182
+ * match.case.idle(() => 'idle'),
183
+ * match.case.loading(() => 'loading'),
184
+ * match.exhaustive
185
+ * );
186
+ * ```
187
+ */
188
+ when: <M>(
189
+ machine: M
190
+ ) => WhenBuilder<Cases, M>;
191
+
192
+ /**
193
+ * API 2b: Case handler creator for pattern matching.
194
+ *
195
+ * @example
196
+ * ```typescript
197
+ * match.case.loading((m) => `Loading: ${m.context.startTime}`)
198
+ * ```
199
+ */
200
+ readonly case: {
201
+ [Name in CaseNames<Cases>]: <R>(
202
+ handler: (machine: CasesToMapping<Cases>[Name]) => R
203
+ ) => CaseHandler<Name, CasesToMapping<Cases>[Name], R>;
204
+ };
205
+
206
+ /**
207
+ * API 2c: Exhaustiveness marker for pattern matching.
208
+ */
209
+ readonly exhaustive: ExhaustivenessMarker;
210
+
211
+ /**
212
+ * API 3: Simple match - returns the name of the matched case or null.
213
+ *
214
+ * @example
215
+ * ```typescript
216
+ * const name = match(machine); // 'idle' | 'loading' | 'success' | null
217
+ * ```
218
+ */
219
+ <M>(machine: M): M extends MatcherUnion<Cases>
220
+ ? CaseNames<Cases> | null
221
+ : null;
222
+ }
223
+
224
+ // =============================================================================
225
+ // SECTION: MATCHER CREATION
226
+ // =============================================================================
227
+
228
+ /**
229
+ * Creates a type-safe matcher for discriminating between machine types.
230
+ *
231
+ * @template Cases - Tuple of [name, MachineType, predicate] configurations
232
+ * @param cases - Array of matcher case definitions
233
+ * @returns A matcher object with three APIs: is (type guards), when (pattern matching), and direct call (simple match)
234
+ *
235
+ * @example
236
+ * ```typescript
237
+ * // Class-based matching
238
+ * const match = createMatcher(
239
+ * ['idle', IdleMachine, (m): m is IdleMachine => m instanceof IdleMachine],
240
+ * ['loading', LoadingMachine, (m): m is LoadingMachine => m instanceof LoadingMachine]
241
+ * );
242
+ *
243
+ * // Or use helper functions
244
+ * const match = createMatcher(
245
+ * classCase('idle', IdleMachine),
246
+ * classCase('loading', LoadingMachine)
247
+ * );
248
+ * ```
249
+ */
250
+ export function createMatcher<
251
+ const Cases extends readonly MatcherCase<string, any, (m: any) => m is any>[]
252
+ >(
253
+ ...cases: Cases
254
+ ): Matcher<Cases> {
255
+ // Build lookup map for O(1) case access
256
+ const nameToCase = new Map<string, { predicate: (m: any) => boolean }>();
257
+
258
+ for (const [name, _, predicate] of cases) {
259
+ if (nameToCase.has(name)) {
260
+ throw new Error(`Duplicate matcher case name: "${name}"`);
261
+ }
262
+ nameToCase.set(name, { predicate });
263
+ }
264
+
265
+ // API 1: Type Guards (using Proxy for dynamic property access)
266
+ const isProxy = new Proxy({} as any, {
267
+ get(_target, prop: string) {
268
+ return function isGuard<M>(machine: M): machine is any {
269
+ const caseConfig = nameToCase.get(prop);
270
+ if (!caseConfig) {
271
+ const available = Array.from(nameToCase.keys()).join(', ');
272
+ throw new Error(
273
+ `Unknown matcher case: "${prop}". Available cases: ${available}`
274
+ );
275
+ }
276
+ return caseConfig.predicate(machine);
277
+ };
278
+ }
279
+ });
280
+
281
+ // API 2b: Case Handlers (using Proxy for dynamic property access)
282
+ const caseProxy = new Proxy({} as any, {
283
+ get(_target, prop: string) {
284
+ return function createCaseHandler<R>(
285
+ handler: (machine: any) => R
286
+ ): CaseHandler<any, any, R> {
287
+ // Validate case name exists
288
+ if (!nameToCase.has(prop)) {
289
+ const available = Array.from(nameToCase.keys()).join(', ');
290
+ throw new Error(
291
+ `Unknown matcher case: "${prop}". Available cases: ${available}`
292
+ );
293
+ }
294
+
295
+ return {
296
+ __brand: 'CaseHandler' as const,
297
+ __name: prop,
298
+ __machine: undefined as any,
299
+ __return: undefined as any,
300
+ handler
301
+ };
302
+ };
303
+ }
304
+ });
305
+
306
+ // API 2c: Exhaustiveness marker
307
+ const exhaustive: ExhaustivenessMarker = { __exhaustive: true };
308
+
309
+ // API 2a: Pattern Matching Builder
310
+ function when<M>(machine: M): WhenBuilder<Cases, M> {
311
+ return {
312
+ is<R>(...handlers: any[]): R {
313
+ // Validate we have at least exhaustiveness marker
314
+ if (handlers.length === 0) {
315
+ throw new Error('Pattern match requires at least one handler and exhaustiveness marker');
316
+ }
317
+
318
+ // Last element should be exhaustiveness marker
319
+ const lastHandler = handlers[handlers.length - 1];
320
+ if (!lastHandler || typeof lastHandler !== 'object' || !('__exhaustive' in lastHandler)) {
321
+ throw new Error(
322
+ 'Pattern match must end with match.exhaustive for compile-time exhaustiveness checking'
323
+ );
324
+ }
325
+
326
+ // Remove exhaustiveness marker
327
+ const actualHandlers = handlers.slice(0, -1) as CaseHandler<any, any, R>[];
328
+
329
+ // Try each handler in order (first-match-wins)
330
+ for (const caseHandler of actualHandlers) {
331
+ const caseName = caseHandler.__name;
332
+ const caseConfig = nameToCase.get(caseName);
333
+
334
+ if (!caseConfig) {
335
+ throw new Error(`Internal error: Unknown matcher case in handler: ${caseName}`);
336
+ }
337
+
338
+ if (caseConfig.predicate(machine)) {
339
+ return caseHandler.handler(machine);
340
+ }
341
+ }
342
+
343
+ // No handler matched - this means pattern match wasn't actually exhaustive at runtime
344
+ const handledCases = actualHandlers.map(h => h.__name).join(', ');
345
+ const allCases = Array.from(nameToCase.keys()).join(', ');
346
+ throw new Error(
347
+ `Non-exhaustive pattern match at runtime: no handler matched the machine.\n` +
348
+ `Handled cases: [${handledCases}]\n` +
349
+ `All cases: [${allCases}]\n` +
350
+ `This may occur if predicates don't cover all runtime possibilities.`
351
+ );
352
+ }
353
+ };
354
+ }
355
+
356
+ // API 3: Simple Match (callable function)
357
+ function simpleMatcher<M>(machine: M): string | null {
358
+ for (const [name, _, predicate] of cases) {
359
+ if (predicate(machine)) {
360
+ return name;
361
+ }
362
+ }
363
+ return null;
364
+ }
365
+
366
+ // Combine all APIs into a single object
367
+ return Object.assign(simpleMatcher, {
368
+ is: isProxy,
369
+ when,
370
+ case: caseProxy,
371
+ exhaustive
372
+ }) as any;
373
+ }
374
+
375
+ // =============================================================================
376
+ // SECTION: HELPER FUNCTIONS
377
+ // =============================================================================
378
+
379
+ /**
380
+ * Creates a class-based matcher case using instanceof checking.
381
+ * This is the most common pattern for Type-State machines.
382
+ *
383
+ * @template Name - The unique name for this case
384
+ * @template T - The class constructor
385
+ * @param name - The name to use for this case
386
+ * @param machineClass - The class to check with instanceof
387
+ * @returns A matcher case tuple
388
+ *
389
+ * @example
390
+ * ```typescript
391
+ * const match = createMatcher(
392
+ * classCase('idle', IdleMachine),
393
+ * classCase('loading', LoadingMachine),
394
+ * classCase('success', SuccessMachine)
395
+ * );
396
+ * ```
397
+ */
398
+ export function classCase<
399
+ Name extends string,
400
+ T extends abstract new (...args: any[]) => any
401
+ >(
402
+ name: Name,
403
+ machineClass: T
404
+ ): MatcherCase<Name, InstanceType<T>, (m: any) => m is InstanceType<T>> {
405
+ return [
406
+ name,
407
+ undefined as any, // Type-only, not used at runtime
408
+ (m): m is InstanceType<T> => m instanceof machineClass
409
+ ] as const;
410
+ }
411
+
412
+ /**
413
+ * Creates a discriminated union matcher case based on a context property.
414
+ * This integrates with the existing hasState utility for context-based discrimination.
415
+ *
416
+ * @template Name - The unique name for this case
417
+ * @template M - The machine type (use Machine<YourContextUnion> for proper narrowing)
418
+ * @template K - The context key to check
419
+ * @template V - The value to match
420
+ * @param name - The name to use for this case
421
+ * @param key - The context property to check
422
+ * @param value - The value the property should equal
423
+ * @returns A matcher case tuple
424
+ *
425
+ * @example
426
+ * ```typescript
427
+ * type FetchContext =
428
+ * | { status: 'idle' }
429
+ * | { status: 'loading' }
430
+ * | { status: 'success'; data: string };
431
+ *
432
+ * const match = createMatcher(
433
+ * discriminantCase<'idle', Machine<FetchContext>, 'status', 'idle'>('idle', 'status', 'idle'),
434
+ * discriminantCase<'loading', Machine<FetchContext>, 'status', 'loading'>('loading', 'status', 'loading'),
435
+ * discriminantCase<'success', Machine<FetchContext>, 'status', 'success'>('success', 'status', 'success')
436
+ * );
437
+ * ```
438
+ */
439
+ export function discriminantCase<
440
+ Name extends string,
441
+ M extends Machine<any> = Machine<any>,
442
+ K extends keyof Context<M> = any,
443
+ V extends Context<M>[K] = any
444
+ >(
445
+ name: Name,
446
+ key: K,
447
+ value: V
448
+ ): MatcherCase<
449
+ Name,
450
+ M & { context: Extract<Context<M>, { [P in K]: V }> },
451
+ (m: M) => m is M & { context: Extract<Context<M>, { [P in K]: V }> }
452
+ > {
453
+ return [
454
+ name,
455
+ undefined as any, // Type-only, not used at runtime
456
+ (m): m is any => hasState(m, key as any, value as any)
457
+ ] as const;
458
+ }
459
+
460
+ /**
461
+ * Creates a custom matcher case with a user-defined predicate.
462
+ * For advanced matching logic beyond instanceof or discriminants.
463
+ *
464
+ * @template Name - The unique name for this case
465
+ * @template M - The machine type this case matches (inferred from predicate)
466
+ * @param name - The name to use for this case
467
+ * @param predicate - A type guard function that determines if a machine matches
468
+ * @returns A matcher case tuple
469
+ *
470
+ * @example
471
+ * ```typescript
472
+ * const match = createMatcher(
473
+ * customCase('complex', (m): m is ComplexMachine => {
474
+ * return m.context.value > 10 && m.context.status === 'active';
475
+ * })
476
+ * );
477
+ * ```
478
+ */
479
+ export function customCase<
480
+ const Name extends string,
481
+ M
482
+ >(
483
+ name: Name,
484
+ predicate: (m: any) => m is M
485
+ ): MatcherCase<Name, M, (m: any) => m is M> {
486
+ return [name, undefined as any, predicate] as const;
487
+ }
488
+
489
+ /**
490
+ * Creates a discriminated matcher builder for a specific context union type.
491
+ * Provides better type inference by capturing the context type upfront.
492
+ *
493
+ * @template C - The discriminated union context type
494
+ * @returns A builder object with a `case` method for defining cases with less boilerplate
495
+ *
496
+ * @example
497
+ * ```typescript
498
+ * type FetchContext =
499
+ * | { status: 'idle' }
500
+ * | { status: 'loading'; startTime: number }
501
+ * | { status: 'success'; data: string };
502
+ *
503
+ * const builder = forContext<FetchContext>();
504
+ *
505
+ * const match = createMatcher(
506
+ * builder.case('idle', 'status', 'idle'),
507
+ * builder.case('loading', 'status', 'loading'),
508
+ * builder.case('success', 'status', 'success')
509
+ * );
510
+ *
511
+ * // Full type inference and narrowing works!
512
+ * if (match.is.success(machine)) {
513
+ * console.log(machine.context.data); // ✓ TypeScript knows data exists
514
+ * }
515
+ * ```
516
+ */
517
+ export function forContext<C extends object>() {
518
+ type MachineWithContext = { readonly context: C };
519
+
520
+ return {
521
+ /**
522
+ * Creates a discriminated union case with full type inference.
523
+ */
524
+ case<
525
+ Name extends string,
526
+ K extends keyof C,
527
+ V extends C[K]
528
+ >(
529
+ name: Name,
530
+ key: K,
531
+ value: V
532
+ ): MatcherCase<
533
+ Name,
534
+ MachineWithContext & { context: Extract<C, { [P in K]: V }> },
535
+ (m: MachineWithContext) => m is MachineWithContext & { context: Extract<C, { [P in K]: V }> }
536
+ > {
537
+ return [
538
+ name,
539
+ undefined as any,
540
+ (m): m is any => hasState(m, key as any, value as any)
541
+ ] as const;
542
+ }
543
+ };
544
+ }