Type Safe
In the previous chapter, we learned how to create commands using declarative configuration with plain JavaScript objects.
While this approach works well, TypeScript users can benefit from enhanced type safety and better development experience using Gunshi's define function.
The define function wraps your command configuration and provides automatic type inference, ensuring that your command handlers receive properly typed context objects without manual type annotations.
Benefits of Type Safety
Using TypeScript with Gunshi's define function provides CLI-specific advantages:
- Autocompletion for command options: IDE suggests available options when accessing
ctx.values - Prevent runtime errors: Catch typos in option names before your CLI ships
- Self-documenting commands: Types show exactly what arguments your command accepts
- Safe refactoring: Rename options across your codebase with confidence
Type Safety Levels in Gunshi
Gunshi provides different levels of type safety to match your needs:
- Basic type inference (covered in this chapter): Automatic typing of command arguments
- Plugin extension typing: Type-safe access to plugin functionality
- Full type parameters: Complete control over all types using
GunshiParams
This chapter focuses on the first level, which covers most common use cases. Advanced patterns are available when you need them.
Using define for Type Safety
The define function transforms your command configuration to provide:
- Automatic type inference: No need to manually type
ctxparameters - IDE autocompletion: Get suggestions for
ctx.valuesproperties - Compile-time validation: TypeScript catches typos and type mismatches before runtime
- Simplified imports: No need to import type definitions like
CommandorCommandContext
Let's transform the greeting command from the previous chapter to use define for full type safety.
The define function is a simple wrapper that preserves your command's type information, enabling TypeScript to automatically infer types for your command options and provide IDE autocompletion:
import { cli, define } from 'gunshi'
// Define a command using the `define` function
const command = define({
name: 'greet',
args: {
// Define a string option 'name' with a short alias 'n'
name: {
type: 'string',
short: 'n',
description: 'Your name'
},
// Define a number option 'age' with a default value
age: {
type: 'number',
short: 'a',
description: 'Your age',
default: 30
},
// Define a boolean flag 'verbose'
verbose: {
type: 'boolean',
short: 'V',
description: 'Enable verbose output'
}
},
// The 'ctx' parameter is automatically typed based on the args
run: ctx => {
// `ctx.values` is fully typed!
const { name, age, verbose } = ctx.values
// TypeScript knows the types:
// - name: string | undefined (undefined if not provided)
// - age: number (always a number due to the default)
// - verbose: boolean | undefined (undefined if not provided, true if --verbose flag is used)
let greeting = `Hello, ${name || 'stranger'}!`
// age always has a value due to the default
greeting += ` You are ${age} years old.`
console.log(greeting)
if (verbose) {
console.log('Verbose mode enabled.')
console.log('Parsed values:', ctx.values)
}
}
})
// Execute the command
await cli(process.argv.slice(2), command)TIP
The example fully code is here.
With define:
- You don't need to import types like
CommandorCommandContext. - The
ctxparameter in therunfunction automatically gets the correct type, derived from theargsdefinition. - Accessing
ctx.values.optionNameprovides type safety and autocompletion based on the option'stypeand whether it has adefault.- Options without a
default(likename) are typed asT | undefined. - Options with a
default(likeage) are typed simply asT. - Boolean flags without a
default(likeverbose) are typed asboolean | undefined.
- Options without a
NOTE
For boolean options that need both positive and negative forms (e.g., --verbose and --no-verbose), see the Negatable Boolean Options section in the declarative configuration guide.
This approach significantly simplifies creating type-safe CLIs with Gunshi.
When to Use define
Use the define function when:
- You're writing TypeScript and want automatic type inference
- You need IDE autocompletion for command context
- You want to catch type-related errors at compile time
Use plain objects (as shown in the previous chapter) when:
- You're writing plain JavaScript
- You prefer explicit type annotations
- You're integrating with existing type definitions
Advanced Type Parameters
While the examples above show the simplest form of the define function, Gunshi provides more advanced type parameter patterns for complex scenarios:
- Plugin extensions: Type-safe access to plugin-provided functionality
- Explicit argument types: Fine-grained control over type inference
- GunshiParams utility: Combined typing of arguments and extensions
These advanced patterns are covered in detail in the Advanced Type System documentation.
For most commands, the basic define usage shown above provides sufficient type safety.
Next Steps
Now that you understand how to create type-safe commands with define, you're ready to explore more advanced features:
- Composable Sub-commands: Learn how type safety extends to multi-command CLIs
- Plugin System: Discover how plugins maintain type safety across extensions
- Advanced Type System: For complex scenarios, Gunshi offers additional type parameters and patterns (covered in the Advanced Type System documentation)
In the next chapter, we'll explore how to create composable sub-commands while maintaining the type safety we've established here.
