zig-cli now includes a fully type-safe API layer that leverages Zig's powerful comptime features to provide compile-time validation, zero runtime overhead, and excellent developer experience.
Before (Runtime API):
const name = ctx.getOption("name"); // Typo in "name" only caught at runtime!
// ^^^^^^ String literal - no compile-time checkingAfter (Typed API):
const name = ctx.get(.name); // Typo caught at compile time!
// ^^^^^ Enum field - validated by compilerThe typed API is implemented using Zig's comptime features, which means:
- All type checking happens at compile time
- No runtime reflection or parsing
- Same performance as hand-written code
- Zero-cost abstraction
- IDE Autocomplete: Full IntelliSense/LSP support for field names
- Type Inference: No need for manual type casting
- Documentation: Self-documenting code through type definitions
- Refactoring: Rename fields with confidence
Auto-generate CLI options from a struct definition.
Example:
const ServerOptions = struct {
port: u16 = 8080,
host: []const u8 = "localhost",
workers: u8 = 4,
verbose: bool = false,
log_level: enum { debug, info, warn, @"error" } = .info,
};
var cmd = try cli.TypedCommand(ServerOptions).init(allocator, "serve", "Start the server");
defer cmd.deinit();What happens under the hood:
- Struct fields → CLI options automatically
- Field types → Option types (string, int, float, bool, enum)
- Optionals (
?T) → Optional CLI options - Default values → Option defaults
- Required fields → Required options
Access parsed options with compile-time validation.
API:
fn serverAction(ctx: *cli.TypedContext(ServerOptions)) !void {
// Individual field access
const port = ctx.get(.port); // u16
const host = ctx.get(.host); // []const u8
const log_level = ctx.get(.log_level); // enum
// Or parse entire struct
const opts = try ctx.parse(); // Returns ServerOptions
std.debug.print("Starting server on {s}:{d}\n", .{opts.host, opts.port});
}Type Safety:
- Field names validated at compile time
- Types automatically inferred
- No optionals for required fields
- Compile error if field doesn't exist
Load configuration files with compile-time schema validation.
Example:
const AppConfig = struct {
app_name: []const u8,
database: struct {
host: []const u8,
port: u16,
username: []const u8,
password: []const u8,
pool_size: u32 = 100,
},
features: struct {
auth: bool = true,
metrics: bool = false,
},
log_level: enum { debug, info, warn, @"error" } = .info,
};
// Load with full type checking
var config = try cli.config.loadTyped(AppConfig, allocator, "config.toml");
defer config.deinit();
// Access fields directly
const db_url = try std.fmt.allocPrint(allocator, "{s}:{d}", .{
config.value.database.host,
config.value.database.port,
});Supported Types:
- ✅ Primitives:
bool,i8-i64,u8-u64,f32,f64 - ✅ Strings:
[]const u8 - ✅ Enums: Any Zig enum
- ✅ Optionals:
?T - ✅ Nested structs: Arbitrary depth
- ✅ Fixed arrays:
[N]T
Validation:
- Missing required fields → Compile error
- Type mismatches → Runtime error with clear message
- Integer overflow → Checked and reported
- Invalid enum values → Error
Type-safe middleware context with compile-time field validation.
Example:
const RequestContext = struct {
request_id: []const u8 = "",
user_id: []const u8 = "",
role: enum { admin, user, guest } = .guest,
authenticated: bool = false,
start_time: i64 = 0,
};
fn authMiddleware(ctx: *cli.TypedMiddleware(RequestContext)) !bool {
// Compile-time validated field access
ctx.set(.user_id, "12345");
ctx.set(.role, .admin); // Enum - type checked!
ctx.set(.authenticated, true);
ctx.set(.start_time, std.time.milliTimestamp());
// Type-safe retrieval
if (ctx.get(.role) == .admin) {
std.debug.print("Admin access\n", .{});
}
return true;
}
// Use in chain
var chain = cli.TypedMiddlewareChain(RequestContext).init(allocator);
defer chain.deinit();
try chain.useTyped(authMiddleware, "auth");
try chain.use(someRegularMiddleware); // Can mix typed and regularBenefits:
- No string keys → Compile-time field validation
- No type casting → Direct type access
- Enum support → Type-safe state management
- IDE support → Autocomplete for all fields
- Comptime Introspection: Uses
@typeInfo()to inspect struct fields at compile time - Code Generation: Generates wrapper code that bridges typed and runtime APIs
- Zero Overhead: All type info resolved at compile time, no runtime cost
- Compatibility: Typed API wraps runtime API, can be mixed freely
Your code:
const Opts = struct {
name: []const u8,
count: u32 = 1,
};Generated (conceptually):
// Options auto-created
Option.init("name", "name", "Auto-generated", .string).withRequired(true);
Option.init("count", "count", "Auto-generated", .int).withDefault("1");
// Type-safe accessor
fn get(ctx, comptime field) FieldType(Opts, field) {
comptime {
// Validates field exists at compile time
_ = std.meta.fieldInfo(Opts, field);
}
const str = ctx.parse_context.getOption(@tagName(field)).?;
return parseValue(FieldType(Opts, field), str);
}Before:
fn myAction(ctx: *cli.Command.ParseContext) !void {
const name_opt = ctx.getOption("name");
const name = name_opt orelse return error.MissingName;
const port_str = ctx.getOption("port") orelse "8080";
const port = try std.fmt.parseInt(u16, port_str, 10);
std.debug.print("Server {s} on port {d}\n", .{name, port});
}After:
const MyOptions = struct {
name: []const u8,
port: u16 = 8080,
};
fn myAction(ctx: *cli.TypedContext(MyOptions)) !void {
const opts = try ctx.parse();
std.debug.print("Server {s} on port {d}\n", .{opts.name, opts.port});
}pub fn TypedCommand(comptime T: type) typeCreates a type-safe command builder from a struct type T.
Methods:
init(allocator, name, description)- Create typed commandsetAction(typed_action)- Set type-safe action functiongetCommand()- Get underlying Command for parsingdeinit()- Clean up resources
pub fn TypedContext(comptime T: type) typeType-safe context for accessing parsed options.
Methods:
get(comptime field)- Get field value with compile-time validationparse()- Parse entire struct at once
pub fn TypedConfig(comptime T: type) typeType-safe config loader with schema validation.
Methods:
load(allocator, path)- Load config from fileloadFromString(allocator, content, format)- Load from stringdiscover(allocator, app_name)- Auto-discover config filedeinit()- Clean up resources
Field: .value: T - The parsed config data
pub fn TypedMiddleware(comptime T: type) typeType-safe middleware context.
Methods:
init(base_context)- Create from base contextset(comptime field, value)- Set field with compile-time validationget(comptime field)- Get field valueparseContext(),command(),allocator()- Access underlying resources
| Feature | Runtime API | Typed API |
|---|---|---|
| Compile Time | ✅ Fast | |
| Runtime Performance | ✅ Good | ✅ Good (identical) |
| Binary Size | ✅ Small | ✅ Small (no overhead) |
| Memory Usage | ✅ Low | ✅ Low (no overhead) |
| Type Safety | ✅ Compile-time | |
| IDE Support | ✅ Full autocomplete |
For new projects, prefer the typed API for better type safety and developer experience.
/// Server configuration options
const ServerOptions = struct {
/// Port to listen on (1-65535)
port: u16 = 8080,
/// Host address to bind
host: []const u8 = "0.0.0.0",
/// Number of worker threads
workers: ?u8 = null,
};const LogLevel = enum { debug, info, warn, @"error" };
const Environment = enum { development, staging, production };
const AppConfig = struct {
log_level: LogLevel = .info,
environment: Environment = .development,
};const BuildOptions = struct {
target: []const u8, // Required
optimize: ?[]const u8 = null, // Optional
verbose: bool = false, // Optional with default
};const AppConfig = struct {
server: struct {
port: u16,
host: []const u8,
},
database: struct {
url: []const u8,
pool_size: u32,
},
features: struct {
auth: bool = true,
metrics: bool = false,
},
};See examples/typed.zig for a comprehensive demonstration of all typed API features.
- Slice Types: Only
[]const u8slices supported (no[]u32, etc.) - Dynamic Arrays: No
ArrayListorstd.ArrayListsupport (use fixed arrays) - Hashmaps: No
HashMapsupport in config structs - Unions: No union type support (except tagged unions/enums)
- Pointers: Only slice pointers supported
- Support for dynamic arrays in config
- Custom validation at struct level
- Automatic help text from doc comments
- Schema export (JSON Schema, OpenAPI)
- Migration tools from runtime to typed API
The typed API provides:
✅ Compile-time safety - Catch errors before runtime ✅ Zero overhead - No performance cost ✅ Better DX - IDE autocomplete, refactoring ✅ Self-documenting - Types are documentation ✅ Gradual adoption - Use alongside runtime API
Use it for maximum type safety in your Zig CLI applications!