Skip to content

Commit 4516aa6

Browse files
boneskullclaude
andcommitted
fix(types): improve type inference for transforms and command handlers
- Add TransformsInput type for contextual typing of inline transform callbacks - Fix any type leakage in transforms.values and transforms.positionals parameters - Add bargs.command<TGlobalOptions>() pattern for proper command handler inference - Update BargsConfig.transforms to use computed types when TTransforms is undefined - Add any-detection (0 extends 1 & T) to preserve compatibility with type assertions - Fix stringPos and numberPos to preserve props type for required inference - Update examples to demonstrate proper type inference patterns - Add BargsConfigWithCommandsInternal for parser internals - Export CommandBuilder and additional inference types BREAKING CHANGE: transforms must appear before handler in config object for TypeScript to infer transformed types in handler parameters 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 70e36c4 commit 4516aa6

14 files changed

Lines changed: 792 additions & 270 deletions
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Command Handler Type Inference
2+
3+
## Problem
4+
5+
When defining commands inline without `bargs.command()`, handler parameters (`values`, `positionals`) become `any`:
6+
7+
```typescript
8+
bargsAsync({
9+
commands: {
10+
create: {
11+
options: { verbose: bargs.boolean() },
12+
handler: ({ values }) => {
13+
// values is 'any' - no type inference!
14+
},
15+
},
16+
},
17+
});
18+
```
19+
20+
**Root cause:** `CommandConfigInput` uses `handler: Handler<any>`, losing type information when TypeScript infers `TCommands extends Record<string, CommandConfigInput>`.
21+
22+
## Solution
23+
24+
Use a mapped type that captures each command's structure and computes the correct handler type, including:
25+
26+
- Global options merged with command options
27+
- Global transforms applied first, then command transforms
28+
- Command positionals through command transforms
29+
30+
### New Type: `InferredCommands`
31+
32+
```typescript
33+
type InferredCommands<
34+
TGlobalOptions extends OptionsSchema,
35+
TGlobalTransforms extends TransformsConfig<any, any, any, any> | undefined,
36+
TCommands extends Record<string, CommandConfigInput>,
37+
> = {
38+
[K in keyof TCommands]: {
39+
description: string;
40+
options?: TCommands[K]['options'];
41+
positionals?: TCommands[K]['positionals'];
42+
transforms?: TCommands[K]['transforms'];
43+
handler: Handler<
44+
BargsResult<
45+
InferTransformedValues<
46+
InferTransformedValues<
47+
InferOptions<TGlobalOptions> &
48+
InferOptions<
49+
TCommands[K]['options'] extends OptionsSchema
50+
? TCommands[K]['options']
51+
: Record<string, never>
52+
>,
53+
TGlobalTransforms
54+
>,
55+
TCommands[K]['transforms']
56+
>,
57+
InferTransformedPositionals<
58+
InferPositionals<
59+
TCommands[K]['positionals'] extends PositionalsSchema
60+
? TCommands[K]['positionals']
61+
: readonly []
62+
>,
63+
TCommands[K]['transforms']
64+
>,
65+
string
66+
>
67+
>;
68+
};
69+
};
70+
```
71+
72+
### Updated `BargsConfigWithCommands`
73+
74+
Use intersection to overlay computed handler types:
75+
76+
```typescript
77+
type BargsConfigWithCommands<
78+
TOptions extends OptionsSchema,
79+
TCommands extends Record<string, CommandConfigInput>,
80+
TTransforms extends TransformsConfig<...> | undefined,
81+
> = {
82+
// ... other properties
83+
commands: TCommands & InferredCommands<TOptions, TTransforms, TCommands>;
84+
};
85+
```
86+
87+
## Behavior
88+
89+
Command handlers receive:
90+
91+
- **values**: `InferOptions<GlobalOpts> & InferOptions<CommandOpts>`, transformed by global then command transforms
92+
- **positionals**: `InferPositionals<CommandPositionals>`, transformed by command transforms
93+
- **command**: `string` (the command name)
94+
95+
## Files to Modify
96+
97+
1. `src/types.ts` - Add `InferredCommands`, update `BargsConfigWithCommands`
98+
2. `src/bargs.ts` - Update function overloads to use new types

eslint.config.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,6 @@ export default defineConfig(
132132
'**/*.snapshot',
133133
'**/.tmp/**/*',
134134
'.worktrees/**/*',
135-
'examples/**/*',
136135
],
137136
},
138137
);

examples/greeter.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,27 @@ import { bargs, bargsAsync } from '../src/index.js';
1818

1919
const optionsDef = bargs.options({
2020
greeting: bargs.string({
21-
description: 'The greeting to use',
22-
default: 'Hello',
2321
aliases: ['g'],
22+
default: 'Hello',
23+
description: 'The greeting to use',
2424
}),
2525
shout: bargs.boolean({
26-
description: 'SHOUT THE GREETING',
27-
default: false,
2826
aliases: ['s'],
27+
default: false,
28+
description: 'SHOUT THE GREETING',
2929
}),
3030
verbose: bargs.boolean({
31-
description: 'Show extra output',
32-
default: false,
3331
aliases: ['v'],
32+
default: false,
33+
description: 'Show extra output',
3434
}),
3535
});
3636

3737
const positionalsDef = bargs.positionals(
3838
bargs.stringPos({
3939
description: 'Name to greet',
40-
required: true,
4140
name: 'name',
41+
required: true,
4242
}),
4343
);
4444

@@ -48,11 +48,11 @@ const main = async () => {
4848
// You can also pass a custom Theme object
4949
const result = await bargsAsync(
5050
{
51-
name: 'greeter',
52-
version: '1.0.0',
5351
description: 'A friendly greeter CLI',
52+
name: 'greeter',
5453
options: optionsDef,
5554
positionals: positionalsDef,
55+
version: '1.0.0',
5656
},
5757
{ theme: 'ocean' },
5858
);

0 commit comments

Comments
 (0)