Skip to content

Commit 5227ba9

Browse files
Stronger query and signal definitions (#1056)
Co-authored-by: Jon Parise <jon@indelible.org>
1 parent ef10355 commit 5227ba9

File tree

5 files changed

+93
-14
lines changed

5 files changed

+93
-14
lines changed

packages/client/src/workflow-options.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export interface WorkflowSignalWithStartOptionsWithoutArgs<SignalArgs extends an
4747
/**
4848
* SignalDefinition or name of signal
4949
*/
50-
signal: SignalDefinition | string;
50+
signal: SignalDefinition<SignalArgs> | string;
5151

5252
/**
5353
* Arguments to invoke the signal handler with

packages/common/src/interfaces.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,43 @@ export type WorkflowQueryType = (...args: any[]) => any;
1515
*/
1616
export type Workflow = (...args: any[]) => WorkflowReturnType;
1717

18+
declare const argsBrand: unique symbol;
1819
/**
1920
* An interface representing a Workflow signal definition, as returned from {@link defineSignal}
2021
*
21-
* @remarks `_Args` can be used for parameter type inference in handler functions and *WorkflowHandle methods.
22+
* @remarks `Args` can be used for parameter type inference in handler functions and *WorkflowHandle methods.
23+
* `Name` can optionally be specified with a string literal type to preserve type-level knowledge of the signal name.
2224
*/
23-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
24-
export interface SignalDefinition<_Args extends any[] = []> {
25+
export interface SignalDefinition<Args extends any[] = [], Name extends string = string> {
2526
type: 'signal';
26-
name: string;
27+
name: Name;
28+
/**
29+
* Virtual type brand to maintain a distinction between {@link SignalDefinition} types with different args.
30+
* This field is not present at run-time.
31+
*/
32+
[argsBrand]: Args;
2733
}
2834

35+
declare const retBrand: unique symbol;
2936
/**
3037
* An interface representing a Workflow query definition as returned from {@link defineQuery}
3138
*
32-
* @remarks `_Args` and `_Ret` can be used for parameter type inference in handler functions and *WorkflowHandle methods.
39+
* @remarks `Args` and `Ret` can be used for parameter type inference in handler functions and *WorkflowHandle methods.
40+
* `Name` can optionally be specified with a string literal type to preserve type-level knowledge of the query name.
3341
*/
34-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
35-
export interface QueryDefinition<_Ret, _Args extends any[] = []> {
42+
export interface QueryDefinition<Ret, Args extends any[] = [], Name extends string = string> {
3643
type: 'query';
37-
name: string;
44+
name: Name;
45+
/**
46+
* Virtual type brand to maintain a distinction between {@link QueryDefinition} types with different args.
47+
* This field is not present at run-time.
48+
*/
49+
[argsBrand]: Args;
50+
/**
51+
* Virtual type brand to maintain a distinction between {@link QueryDefinition} types with different return types.
52+
* This field is not present at run-time.
53+
*/
54+
[retBrand]: Ret;
3855
}
3956

4057
/** Get the "unwrapped" return type (without Promise) of the execute handler from Workflow type `W` */

packages/common/src/workflow-handle.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ export interface BaseWorkflowHandle<T extends Workflow> {
2121
* await handle.signal(incrementSignal, 3);
2222
* ```
2323
*/
24-
signal<Args extends any[] = []>(def: SignalDefinition<Args> | string, ...args: Args): Promise<void>;
24+
signal<Args extends any[] = [], Name extends string = string>(
25+
def: SignalDefinition<Args, Name> | string,
26+
...args: Args
27+
): Promise<void>;
2528

2629
/**
2730
* The workflowId of the current Workflow
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import test from 'ava';
2+
import { defineSignal, defineQuery } from '@temporalio/workflow';
3+
4+
test('SignalDefinition Name type safety', (t) => {
5+
// @ts-expect-error Assert expect a type error when generic and concrete names do not match
6+
defineSignal<[string], 'mismatch'>('illegal value');
7+
8+
const signalA = defineSignal<[string], 'a'>('a');
9+
const signalB = defineSignal<[string], 'b'>('b');
10+
11+
type TypeAssertion = typeof signalB extends typeof signalA ? 'intermixable' : 'not-intermixable';
12+
13+
const _assertion: TypeAssertion = 'not-intermixable';
14+
t.pass();
15+
});
16+
17+
test('SignalDefinition Args type safety', (t) => {
18+
const signalString = defineSignal<[string]>('a');
19+
const signalNumber = defineSignal<[number]>('b');
20+
21+
type TypeAssertion = typeof signalNumber extends typeof signalString ? 'intermixable' : 'not-intermixable';
22+
23+
const _assertion: TypeAssertion = 'not-intermixable';
24+
t.pass();
25+
});
26+
27+
test('QueryDefinition Name type safety', (t) => {
28+
// @ts-expect-error Assert expect a type error when generic and concrete names do not match
29+
defineQuery<void, [string], 'mismatch'>('illegal value');
30+
31+
const queryA = defineQuery<void, [string], 'a'>('a');
32+
const queryB = defineQuery<void, [string], 'b'>('b');
33+
34+
type TypeAssertion = typeof queryB extends typeof queryA ? 'intermixable' : 'not-intermixable';
35+
36+
const _assertion: TypeAssertion = 'not-intermixable';
37+
t.pass();
38+
});
39+
40+
test('QueryDefinition Args and Ret type safety', (t) => {
41+
const retVariantA = defineQuery<string>('a');
42+
const retVariantB = defineQuery<number>('b');
43+
44+
type RetTypeAssertion = typeof retVariantB extends typeof retVariantA ? 'intermixable' : 'not-intermixable';
45+
46+
const _retAssertion: RetTypeAssertion = 'not-intermixable';
47+
48+
const argVariantA = defineQuery<string, [number]>('a');
49+
const argVariantB = defineQuery<string, [string]>('b');
50+
51+
type ArgTypeAssertion = typeof argVariantB extends typeof argVariantA ? 'intermixable' : 'not-intermixable';
52+
53+
const _argAssertion: ArgTypeAssertion = 'not-intermixable';
54+
t.pass();
55+
});

packages/workflow/src/workflow.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1122,11 +1122,13 @@ function conditionInner(fn: () => boolean): Promise<void> {
11221122
* Definitions are used to register handler in the Workflow via {@link setHandler} and to signal Workflows using a {@link WorkflowHandle}, {@link ChildWorkflowHandle} or {@link ExternalWorkflowHandle}.
11231123
* Definitions can be reused in multiple Workflows.
11241124
*/
1125-
export function defineSignal<Args extends any[] = []>(name: string): SignalDefinition<Args> {
1125+
export function defineSignal<Args extends any[] = [], Name extends string = string>(
1126+
name: Name
1127+
): SignalDefinition<Args, Name> {
11261128
return {
11271129
type: 'signal',
11281130
name,
1129-
};
1131+
} as SignalDefinition<Args, Name>;
11301132
}
11311133

11321134
/**
@@ -1135,11 +1137,13 @@ export function defineSignal<Args extends any[] = []>(name: string): SignalDefin
11351137
* Definitions are used to register handler in the Workflow via {@link setHandler} and to query Workflows using a {@link WorkflowHandle}.
11361138
* Definitions can be reused in multiple Workflows.
11371139
*/
1138-
export function defineQuery<Ret, Args extends any[] = []>(name: string): QueryDefinition<Ret, Args> {
1140+
export function defineQuery<Ret, Args extends any[] = [], Name extends string = string>(
1141+
name: Name
1142+
): QueryDefinition<Ret, Args, Name> {
11391143
return {
11401144
type: 'query',
11411145
name,
1142-
};
1146+
} as QueryDefinition<Ret, Args, Name>;
11431147
}
11441148

11451149
/**

0 commit comments

Comments
 (0)