ReviewOS

home-lang/home

fix(ts): narrow Promise unions after terminating instanceof guards

#687
Closed chris opened this 8 days ago ·
8 days ago

Parent

Problem

Home does not preserve the non-Promise constituent after a terminating instanceof Promise guard. This leaves local T | Promise<T> values as the full union and degrades imported projected members to any, which removes contextual types from downstream array callbacks.

The concrete Zod 4.5.2 consequence is four TS7006 diagnostics in src/v4/core/parse.ts at the result.issues.map((iss) => ...) callbacks in sync/async parse and safe-parse paths.

Fair oracle

Two modules, with the producer exporting the ordinary structural types below:

export type MaybeAsync<T> = T | Promise<T>;
export interface Payload { value: unknown; issues: string[]; }
export interface SchemaInternals {
  run(payload: Payload): MaybeAsync<Payload>;
}
export interface Schema { _zod: SchemaInternals; }

Consumer:

import type { Schema } from "./schema.js";

export function parse(schema: Schema): unknown {
  const result = schema._zod.run({ value: undefined, issues: [] });
  if (result instanceof Promise) throw new Error("async");
  result.issues.map((issue) => issue.toUpperCase());
  return result.value;
}

Controls establish that a directly returned imported Payload already provides string callback context, while the same MaybeAsync shape declared locally also fails to narrow.

Compiler options: strict, noEmit, skipLibCheck, target: ES2022, module/moduleResolution: NodeNext.

Current and expected

  • TypeScript 6.0.3: clean
  • tsgo native preview 7.0.0-dad1862: clean
  • Home ReleaseSafe: imported oracle reports TS7006; local control additionally retains TS2339 on issues and value

Expected: clean, with the false/continuation branch narrowed to Payload and issue contextually typed as string.

Acceptance

  • Implement structural instanceof Promise union subtraction without source-name or project-specific special cases.
  • Cover local and imported/module-resolved T | Promise<T> returns.
  • Preserve contextual array callback element types after narrowing.
  • Add focused regression tests.
  • Re-run the exact Zod diagnostic baseline and introduce zero new diagnostic identities.
  • Publish detailed benchmark/correctness evidence and update the README summary table.
8 days ago

Implementation progress:

  • Removed the unsuccessful consumer-side provenance experiments; the candidate now has only general type-system changes.
  • instanceof Promise false-branch subtraction recognizes structurally lowered Promise/PromiseLike instances through their real hidden promise payload marker.
  • Complete Program schemas now admit losslessly serialized parameterized built-ins (Promise<T> / PromiseLike<T>), instead of retaining the legacy projection-only gate from #657.
  • Focused checker regression passes.
  • Full ts_program suite passes.
  • Fair two-module oracle is clean under TypeScript 6.0.3, tsgo 7 preview, and the candidate Home compiler (all exit 0, zero diagnostics). The oracle covers imported alias, imported inline union, direct exported function, synchronous control, and local control.

Next: exact Zod diagnostic identity A/B against parent 4d5d3d5a9, then full optimized-build/test/lint verification.

8 days ago

Final candidate evidence before commit:

  • Rejected whole-constraint projection: it did not fix the four parse.ts sites and added 12 unrelated Zod identities. Removed completely.
  • Retained implementation is general: parameterized Program built-ins are admitted losslessly; structurally lowered Promise instances participate in instanceof false-branch subtraction; multi-statement member-assigned functions compare their inferred return union instead of failing unconditionally.
  • TypeScript 6.0.3, tsgo 7 preview, and Home all exit 0 with zero diagnostics on the focused multi-module valid oracle. A separate invalid-control regression retains exactly one TS2322.
  • Full Program suite: pass.
  • Pinned 106-file Zod 4.5.2 exact-parent A/B (4d5d3d5a9): 350 -> 348 diagnostics; 345 -> 343 unique path/line/column/code identities; exactly 2 TS7006 removed (core/schemas.ts:3167:45 and 3256:45); 0 added.
  • Two sequential ReleaseSafe candidate runs are identical after timing removal: SHA-256 11fa7bc0af25a25aa821eae7a0b26704a821f72b0bdca5adbd350a7f19deec13; wall times 61.80s and 58.21s; stdout empty.
  • The separate qualified $ZodRawIssue[] leaf that keeps four parse.ts callbacks opaque is now tracked honestly in #688.

Remaining before push: full checker target, ReleaseFast, repository lint baseline, docs/README.

8 days ago

Code commit 901c0d0ec is now on origin/main (direct push, conventional subject, Chris Breuer identity, no coauthors).

Final code gates:

  • focused three-engine multi-module oracle: clean valid case; exact one-TS2322 invalid control
  • zig build test -Dfilter=ts_program: 205/205 pass
  • zig build test -Dfilter=ts_checker: 4,365/4,365 pass
  • ReleaseSafe build: pass
  • ReleaseFast build: pass
  • zig fmt --check: pass
  • git diff --check: pass
  • bunx --bun pickier .: unchanged repository baseline, 22,427 findings (11,834 errors, 10,593 warnings)

Documentation/README commit is next; issue remains open until that lands.

8 days ago

Shipped directly to origin/main in two small conventional commits:

  • code: 901c0d0ec203e51c84a4a914d05976728a4b3bcf
  • docs: ca6870a7947180dc4659e798cc57a00c06935046

The pinned Zod graph moves from 350 to 348 diagnostics and from 345 to 343 unique identities, removing exactly the two intended TS7006 reports with zero additions. The three-engine oracle is clean on all valid paths and produces the exact single TS2322 negative control. Program 205/205 and checker 4,365/4,365 pass; ReleaseSafe, ReleaseFast, formatting, and diff checks pass. The remaining qualified issue-array transfer is isolated in #688.

Sign in to comment on this issue.