Skip to content

Commit ac02c0f

Browse files
authored
fix(core): drop unique-symbol brand on LocalsKey to fix dual-package builds (#3626)
## Summary `LocalsKey<T>` (the type returned by `locals.create()`) was branded with a module-level `declare const __local: unique symbol`. Each such declaration is its own nominal type, and `tshy` emits separate `.d.ts` files for the ESM and CJS outputs — each gets its own `__local` symbol. Under certain pnpm hoisting layouts a single TypeScript compilation can resolve `LocalsKey` from both the ESM source path and the CJS dist path within the same call site, producing two structurally-incompatible variants of the same type. TS surfaces this as the misleading error: ``` Argument of type 'LocalsKey<X>' is not assignable to parameter of type 'LocalsKey<X>'. Property '[__local]' is missing in type 'LocalsKey<X>' but required in type 'BrandLocal<X>'. ``` The error has been hitting CI on PRs opened since the chat.agent stack landed (e.g. #3625 typecheck job), but doesn't reproduce on developer machines where the pnpm node_modules layout was built up incrementally. ## Fix Replace the `unique symbol` brand with an optional phantom field that carries `T` at the type level: ```ts // before declare const __local: unique symbol; type BrandLocal<T> = { [__local]: T }; export type LocalsKey<T> = BrandLocal<T> & { readonly id: string; readonly __type: unique symbol; }; // after export type LocalsKey<T> = { readonly id: string; readonly __type: symbol; /** Phantom carrier for the value type — never read at runtime. */ readonly __valueType?: T; }; ``` The ESM and CJS `.d.ts` outputs now produce structurally identical types, so cross-output resolution no longer produces a mismatch. `T` is still carried at the type level via the optional phantom field. The runtime shape is unchanged — `manager.ts` was already casting via `as unknown`, which is no longer needed. ## Test plan - [ ] `pnpm run typecheck --filter @trigger.dev/core --filter @trigger.dev/sdk` - [ ] `pnpm run build --filter @trigger.dev/core --filter @trigger.dev/sdk` (clean rebuild) — confirms the ESM and CJS dist `.d.ts` outputs no longer carry distinct `unique symbol` declarations - [ ] `pnpm --filter @trigger.dev/core test test/mockTaskContext.test.ts --run` - [ ] `pnpm --filter @trigger.dev/sdk test test/mockChatAgent.test.ts --run`
1 parent 0510fd6 commit ac02c0f

3 files changed

Lines changed: 25 additions & 8 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@trigger.dev/core": patch
3+
---
4+
5+
Fix `LocalsKey<T>` type incompatibility across dual-package builds. The phantom value-type brand no longer uses a module-level `unique symbol`, so a single TypeScript compilation that resolves the type from both the ESM and CJS outputs (which can happen under certain pnpm hoisting layouts) no longer sees two structurally-incompatible variants of the same type.

packages/core/src/v3/locals/manager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export class NoopLocalsManager implements LocalsManager {
55
return {
66
__type: Symbol(),
77
id,
8-
} as unknown as LocalsKey<T>;
8+
};
99
}
1010

1111
getLocal<T>(key: LocalsKey<T>): T | undefined {
@@ -23,7 +23,7 @@ export class StandardLocalsManager implements LocalsManager {
2323
return {
2424
__type: key,
2525
id,
26-
} as unknown as LocalsKey<T>;
26+
};
2727
}
2828

2929
getLocal<T>(key: LocalsKey<T>): T | undefined {

packages/core/src/v3/locals/types.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
1-
declare const __local: unique symbol;
2-
type BrandLocal<T> = { [__local]: T };
3-
4-
// Create a type-safe store for your locals
5-
export type LocalsKey<T> = BrandLocal<T> & {
1+
/**
2+
* A type-safe key for `locals`. Carries the value type `T` as a phantom
3+
* marker on the optional `__valueType` field so two keys with different
4+
* value types are distinguishable at the type level.
5+
*
6+
* The phantom field is intentionally not anchored to a `unique symbol`:
7+
* dual-package builds (`tshy`) emit separate `.d.ts` files for ESM and
8+
* CJS outputs, and each `unique symbol` declaration in a `.d.ts` is its
9+
* own nominal type. If a single compilation ever resolves `LocalsKey`
10+
* from both the ESM and CJS paths — which happens under certain pnpm
11+
* hoisting layouts — `unique symbol` brands produce structurally
12+
* incompatible variants of the same type. A plain string brand avoids
13+
* the hazard.
14+
*/
15+
export type LocalsKey<T> = {
616
readonly id: string;
7-
readonly __type: unique symbol;
17+
readonly __type: symbol;
18+
/** Phantom carrier for the value type — never read at runtime. */
19+
readonly __valueType?: T;
820
};
921

1022
export interface LocalsManager {

0 commit comments

Comments
 (0)