Skip to content

Support TS function overloads in module mode#214

Open
mmkal wants to merge 5 commits into
mainfrom
module-overloads
Open

Support TS function overloads in module mode#214
mmkal wants to merge 5 commits into
mainfrom
module-overloads

Conversation

@mmkal

@mmkal mmkal commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Module mode currently detects TS overload signatures but only uses the first one. This PR turns each overload signature into an alternate calling convention for the command - overloads being the natural TypeScript idiom for "this function can be called in two different ways":

// commands.ts
/** resize by explicit dimensions */
export function resize(params: {input: string; width: number; height: number}): Promise<string>
/** resize by scale factor, preserving aspect ratio */
export function resize(params: {input: string; scale: number}): Promise<string>
export function resize(params: any) {
  // the implementation dispatches on the input's shape, as overload implementations always do
}
$ mycli resize --help
Usage: mycli resize --input <string> --width <number> --height <number>  # resize by explicit dimensions
       mycli resize --input <string> --scale <number>                    # resize by scale factor, preserving aspect ratio

resize by explicit dimensions

Options:
  --input [string]   path to the input image
  --width [number]
  --height [number]
  --scale [number]
  -h, --help         display help for command

$ mycli resize --input a.png --scale 0.5     # matches signature 2, function gets the validated object
$ mycli resize --input a.png --width 5 --scale 2
error: option '--width [number]' cannot be used with option '--scale [number]'

$ mycli resize --input a.png
Invalid input: ✖ matched none of the 2 ways to call this command:
  --input <string> --width <number> --height <number>: must have required properties width, height
  --input <string> --scale <number>: must have required properties scale

How it works:

  • Extraction: all body-less overload signatures are kept (in declaration order) instead of just the first; the implementation signature is still ignored, so a widened params: any implementation can't poison the command. Shared between functions and class methods.
  • Flags: each signature's schema is built with the existing machinery and combined as anyOf - the existing union-of-objects flag derivation merges the flag lists, and incompatiblePropertyPairs + commander conflicts already reject mixing flags that never co-occur in a signature. Descriptions/@alias documented on one signature are propagated to the same-named flag in others (the merge otherwise keeps the last occurrence).
  • Validation: a hand-attached ~standard validator checks each signature in declaration order (mirroring TS overload resolution) and passes the first match to the function - which needs no "which overload" marker, since overload implementations dispatch on shape anyway. No-match errors list each signature's issues, closest match (fewest issues) first.
  • Help: one usage line per signature with its jsdoc as an aligned # comment, via meta.usage. A small index.ts improvement makes multi-line meta.usage arrays render aligned under Usage: with the full command prefix repeated (previously continuation lines rendered unindented at column 0) - this benefits anyone passing a usage array in any mode.

Scope: only overloads where every signature takes a single object(-like) parameter participate. Signatures with positional parameters keep the previous first-signature-wins behavior, since commander has no way to present alternate positional layouts for one command (pinned by the existing greet test).

Would this be worthwhile in trpc/orpc mode?

Mostly it already exists there: overloads are a TS-syntax concept with no runtime schema equivalent, and the natural way to express "two calling conventions" with zod et al is z.union([z.object(...), z.object(...)]) - which trpc-cli already supports (merged flags, conflicts, union validation). What trpc/orpc mode doesn't get is the presentation layer added here: per-variant usage lines with per-variant descriptions and the closest-match-first error grouping. Adding that would mean detecting top-level unions in parse-procedure and deriving usage lines + a variant-aware validator wrapper from the JSON schema (using each branch's description as the comment). Moderate effort, no new concepts, and it would make the two modes consistent - worthwhile as a follow-up if union-input CLIs turn out to be common, but not tricky enough to block this PR on.

Task file: tasks/module-overloads.md.

🤖 Generated with Claude Code

mmkal and others added 2 commits July 8, 2026 17:17
Support TS function overloads in module mode: each overload signature becomes an
alternate calling convention for the command - merged flags, first-match validation
in declaration order, per-overload usage lines in help.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Overload signatures that each take a single object parameter become alternate
calling conventions for one command: help shows a usage line per signature (with
its jsdoc as an aligned trailing comment), the flags list is the union of all
signatures' flags (flags never sharing a signature conflict), and validation
checks each signature in declaration order - mirroring TS overload resolution -
passing the first match to the function. No-match errors report each signature's
issues, closest match (fewest issues) first.

Signatures with positional parameters keep the previous first-signature-wins
behavior, since commander can't present alternate positional layouts.

Also aligns multi-line meta.usage rendering under 'Usage: ' with the command
prefix repeated (previously continuation lines rendered at column 0).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pkg-pr-new

pkg-pr-new Bot commented Jul 8, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/mmkal/trpc-cli@214

commit: c136182

Comment thread test/typebox-module-commands.test.ts Outdated
Comment thread test/typebox-module-commands.test.ts Outdated
mmkal and others added 2 commits July 8, 2026 17:53
…annotations, jsdoc merging

- An overloaded command's description no longer poses the first signature's jsdoc
  as universal: the implementation signature's jsdoc (previously ignored) becomes
  the overall command description, falling back to the signatures' distinct
  descriptions joined with ' / '. @alias is honored wherever declared.
- Flag descriptions advertise incompatibilities ("Do not use with: --scale"),
  derived from the same incompatiblePropertyPairs that powers commander conflicts.
  Applied generically in index.ts, so zod/trpc union inputs benefit too.
- A flag documented differently across signatures shows every distinct description
  joined with ' / ' instead of last-write-wins; single-signature jsdoc propagation
  is now pinned by tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…roperly

CI lints README code fences; `params: any` tripped no-explicit-any. A widened
union type is better documentation anyway, and showcases the implementation-jsdoc
-> command-description convention.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mmkal mmkal marked this pull request as ready for review July 8, 2026 21:40

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — one minor decision-log inconsistency inline.

Reviewed changes — support for TS function overloads in module mode, plus multi-line meta.usage alignment and conflict annotations for union-derived flags.

  • Overload extraction in src/module-commands.ts keeps all body-less signatures via groupOverloadDeclarations, applies to exported functions and class methods, and ignores the implementation signature.
  • Alternate calling conventions via buildOverloadedProcedure: per-signature usage lines, union flag merging backed by incompatiblePropertyPairs and commander conflicts, first-match validation with closest-match-first errors.
  • Help formatting in src/index.ts: meta.usage arrays render aligned under Usage: by repeating the full command prefix.
  • Docs and tests: README overload example, new module-mode overload tests, and updated migration snapshots for the new "Do not use with:" flag annotations.

Note: the first full-suite run hit the default 10s timeout in test/typebox-module-commands.test.ts; rerunning with a 30s timeout passed all 403 tests.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Kimi K2 (free via Pullfrog for OSS) | 𝕏

Comment thread tasks/module-overloads.md
Comment on lines +61 to +63
- **Per-overload jsdoc**: the first signature's jsdoc remains the command description;
each signature's jsdoc (first line) becomes its usage line's comment. `@alias` is only
honored on the first signature's jsdoc.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- **Per-overload jsdoc**: the first signature's jsdoc remains the command description;
each signature's jsdoc (first line) becomes its usage line's comment. `@alias` is only
honored on the first signature's jsdoc.
- **Per-overload jsdoc**: the implementation-signature jsdoc (when present) becomes the command description; each signature's jsdoc (first line) becomes its usage line's comment. `@alias` is honored anywhere it's declared (implementation or any signature).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant