Skip to content

Commit 047c01b

Browse files
pranaygpv0claude
authored
Make start() types unknown when deploymentId is provided (#1367)
* fix: update types and documentation for start function overloads Ensure types are 'unknown[]' and 'unknown' for 'deploymentId' and update exports and documentation. Slack-Thread: https://vercel.slack.com/archives/C09G3EQAL84/p1773368990070059?thread_ts=1773368990.070059&cid=C09G3EQAL84 Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com> * fix: use generics in deploymentId overloads to avoid contravariance issue Addresses PR review feedback: typed workflows like WorkflowFunction<[string], number> were not assignable to WorkflowFunction<unknown[], unknown> under strictFunctionTypes. Changed to generic parameters while keeping Run<unknown> return type. Also adds type-level tests for overload resolution. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: add changeset for start() deploymentId type changes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: v0 <v0[bot]@users.noreply.github.com> Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4429078 commit 047c01b

6 files changed

Lines changed: 113 additions & 18 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@workflow/core": patch
3+
---
4+
5+
Make `start()` return `Run<unknown>` with `unknown[]` args when `deploymentId` is provided, since the deployed workflow version may have different types

‎docs/content/docs/api-reference/workflow-api/start.mdx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ Learn more about [`WorkflowReadableStreamOptions`](/docs/api-reference/workflow-
5454
* This is different from calling workflow functions directly, which is the typical pattern in Next.js applications.
5555
* The function returns immediately after enqueuing the workflow - it doesn't wait for the workflow to complete.
5656
* All arguments must be [serializable](/docs/foundations/serialization).
57+
* When `deploymentId` is provided, the argument types and return type become `unknown` since there is no guarantee the workflow function's types will be consistent across different deployments.
5758

5859
## Examples
5960

‎packages/core/src/runtime.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,13 @@ export {
7171
type StopSleepResult,
7272
wakeUpRun,
7373
} from './runtime/runs.js';
74-
export { type StartOptions, start } from './runtime/start.js';
74+
export {
75+
type StartOptions,
76+
type StartOptionsBase,
77+
type StartOptionsWithDeploymentId,
78+
type StartOptionsWithoutDeploymentId,
79+
start,
80+
} from './runtime/start.js';
7581
export { stepEntrypoint } from './runtime/step-handler.js';
7682
export {
7783
createWorld,

‎packages/core/src/runtime/start.test.ts‎

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
import { WorkflowRuntimeError } from '@workflow/errors';
22
import { SPEC_VERSION_CURRENT, SPEC_VERSION_LEGACY } from '@workflow/world';
3-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3+
import {
4+
afterEach,
5+
beforeEach,
6+
describe,
7+
expect,
8+
expectTypeOf,
9+
it,
10+
vi,
11+
} from 'vitest';
12+
import type { Run } from './run.js';
413
import { start } from './start.js';
14+
import type { WorkflowFunction } from './start.js';
515
import { getWorld } from './world.js';
616

717
// Mock @vercel/functions
@@ -380,4 +390,47 @@ describe('start', () => {
380390
);
381391
});
382392
});
393+
394+
describe('overload type inference', () => {
395+
// Type-only assertions that don't execute start() at runtime.
396+
// We use expectTypeOf on the function signature's return type directly.
397+
398+
type TypedWf = WorkflowFunction<[string, number], boolean>;
399+
type ZeroArgWf = WorkflowFunction<[], string>;
400+
type Meta = { workflowId: string };
401+
402+
it('should preserve types without deploymentId', () => {
403+
// With args
404+
expectTypeOf<
405+
(wf: TypedWf, args: [string, number]) => Promise<Run<boolean>>
406+
>().toMatchTypeOf<typeof start>();
407+
408+
// Zero-arg workflow without args
409+
expectTypeOf(start<string>)
410+
.parameter(0)
411+
.toMatchTypeOf<ZeroArgWf | Meta>();
412+
});
413+
414+
it('should return Run<unknown> when deploymentId is provided', () => {
415+
// Typed workflow with deploymentId - return type becomes Run<unknown>
416+
type StartWithDeploymentId = (
417+
wf: TypedWf | Meta,
418+
args: unknown[],
419+
opts: { deploymentId: string }
420+
) => Promise<Run<unknown>>;
421+
expectTypeOf<StartWithDeploymentId>().toMatchTypeOf<typeof start>();
422+
});
423+
424+
it('should accept typed workflows with deploymentId (no contravariance issue)', () => {
425+
// This is the key test: a typed workflow should be assignable to the
426+
// deploymentId overload. We verify by checking the first parameter
427+
// accepts TypedWf.
428+
type DeploymentIdOverload = <TArgs extends unknown[], TResult>(
429+
wf: WorkflowFunction<TArgs, TResult> | Meta,
430+
args: unknown[],
431+
opts: { deploymentId: string }
432+
) => Promise<Run<unknown>>;
433+
expectTypeOf<DeploymentIdOverload>().toMatchTypeOf<typeof start>();
434+
});
435+
});
383436
});

‎packages/core/src/runtime/start.ts‎

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,20 @@ import { getWorld } from './world.js';
1717
/** ULID generator for client-side runId generation */
1818
const ulid = monotonicFactory();
1919

20-
export interface StartOptions {
20+
export interface StartOptionsBase {
21+
/**
22+
* The world to use for the workflow run creation,
23+
* by default the world is inferred from the environment variables.
24+
*/
25+
world?: World;
26+
27+
/**
28+
* The spec version to use for the workflow run. Defaults to the latest version.
29+
*/
30+
specVersion?: number;
31+
}
32+
33+
export interface StartOptionsWithDeploymentId extends StartOptionsBase {
2134
/**
2235
* The deployment ID to use for the workflow run.
2336
*
@@ -27,21 +40,24 @@ export interface StartOptions {
2740
* Set to `'latest'` to automatically resolve the most recent deployment
2841
* for the current environment (same production target or git branch).
2942
* This is currently a Vercel-specific feature.
43+
*
44+
* **Note:** When `deploymentId` is provided, the argument and return types become `unknown`
45+
* since there is no guarantee the types will be consistent across deployments.
3046
*/
31-
deploymentId?: 'latest' | (string & {});
32-
33-
/**
34-
* The world to use for the workflow run creation,
35-
* by default the world is inferred from the environment variables.
36-
*/
37-
world?: World;
47+
deploymentId: 'latest' | (string & {});
48+
}
3849

39-
/**
40-
* The spec version to use for the workflow run. Defaults to the latest version.
41-
*/
42-
specVersion?: number;
50+
export interface StartOptionsWithoutDeploymentId extends StartOptionsBase {
51+
deploymentId?: undefined;
4352
}
4453

54+
/**
55+
* Options for starting a workflow run.
56+
*/
57+
export type StartOptions =
58+
| StartOptionsWithDeploymentId
59+
| StartOptionsWithoutDeploymentId;
60+
4561
/**
4662
* Represents an imported workflow function.
4763
*/
@@ -62,15 +78,30 @@ export type WorkflowMetadata = { workflowId: string };
6278
* @param options - The options for the workflow run (optional).
6379
* @returns The unique run ID for the newly started workflow invocation.
6480
*/
81+
// Overloads with deploymentId - args and return type become unknown
82+
// Uses generics so typed workflows are assignable (avoids contravariance issues),
83+
// but the return type and args are still unknown since the deployed version may differ.
84+
export function start<TArgs extends unknown[], TResult>(
85+
workflow: WorkflowFunction<TArgs, TResult> | WorkflowMetadata,
86+
args: unknown[],
87+
options: StartOptionsWithDeploymentId
88+
): Promise<Run<unknown>>;
89+
90+
export function start<TResult>(
91+
workflow: WorkflowFunction<[], TResult> | WorkflowMetadata,
92+
options: StartOptionsWithDeploymentId
93+
): Promise<Run<unknown>>;
94+
95+
// Overloads without deploymentId - preserve type inference
6596
export function start<TArgs extends unknown[], TResult>(
6697
workflow: WorkflowFunction<TArgs, TResult> | WorkflowMetadata,
6798
args: TArgs,
68-
options?: StartOptions
99+
options?: StartOptionsWithoutDeploymentId
69100
): Promise<Run<TResult>>;
70101

71102
export function start<TResult>(
72103
workflow: WorkflowFunction<[], TResult> | WorkflowMetadata,
73-
options?: StartOptions
104+
options?: StartOptionsWithoutDeploymentId
74105
): Promise<Run<TResult>>;
75106

76107
export async function start<TArgs extends unknown[], TResult>(

‎packages/core/src/serialization.ts‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -570,8 +570,7 @@ export class WorkflowServerWritableStream extends WritableStream<Uint8Array> {
570570
// unsettled promise because the cleared timer will never fire.
571571
const waiters = flushWaiters;
572572
flushWaiters = [];
573-
const abortError =
574-
reason ?? new Error("Stream aborted");
573+
const abortError = reason ?? new Error('Stream aborted');
575574
for (const w of waiters) w.reject(abortError);
576575
},
577576
});

0 commit comments

Comments
 (0)