中文

Introduction

Build TypeScript CLIs with func that balance developer experience, maintainability, runtime performance, and artifact size.

Updated 2 weeks ago

func is a lightweight CLI application framework for TypeScript. It uses classes and decorators to declare commands, and provides input definition and validation, command dispatch, error boundaries, service injection, and a range of type-safety features. It is a complete solution for terminal projects, covering the entire workflow from local development to production builds.

You can start small at any time—with a single-command tool or a single-file project—and split out handlers, services, and modules only as commands and business rules grow. The same input model, runtime, and build workflow stay with you throughout, keeping development efficient and predictable. Because func delivers a strong developer experience, maintainability, and extensibility while remaining fast and producing small bundles, it is a good fit at every stage of your project.

Why func?

The complexity of a CLI project usually rises quickly. Even a modest extension can make the code difficult to understand and maintain, leaving the project trapped in endless patches and redundant defensive programming. A single input may need a name, type, default, validation rules, and an error message; commands may need to share filesystem access, network requests, and common business rules; after release, existing invocation patterns still need to remain compatible.

If all these responsibilities are packed into argument parsing and command callbacks, changing even one option affects parsing, validation, execution, and error handling at the same time. As features accumulate, every modification requires understanding and verifying an ever-larger portion of the codebase.

Clear boundaries from the first command The same structure applies from the first command; as commands, rules, and dependencies accumulate, responsibilities remain clear. func argument parser + local structure
Maintainability easy to maintain hard to maintain 1 command many commands shared services cross-field rules tests & release project complexity → func argument parser + local structure
This chart compares how two approaches scale as a project grows: func keeps maintenance manageable through clear responsibility boundaries, while relying on an argument parser and local structure becomes harder to maintain as commands, rules, and dependencies accumulate.

func provides ready-made capabilities for common CLI requirements and requires every handler to strictly follow TypeScript types. This allows a project to support more—and more complex—business modules. Each change concerns only core business code without intruding on the framework; you do not even need to understand how it works internally.

Overall considerations

Bundle size, cold start, DX, and maintainability Each point is positioned by bundle size, cold-start time, and DX; larger points indicate a higher maintainability proxy.
0 100 200 300 350 35 45 55 65 75 25 50 75 100 bundle size (KiB) cold start (ms) DX func 45.0 KiB · 40.1 ms Commander 44.9 KiB · 42.0 ms yargs 115.1 KiB · 71.7 ms @oclif/core 331.3 KiB · 70.8 ms cac 17.0 KiB · 38.8 ms
Bundle and startup values come from benchmarks/report.json. The axes use linear scales of 0–350 KiB, 35–75 ms, and DX 0–100. DX and maintainability come from the report authoring evaluation: every criterion is graded from 0 to 4 and converted to 100 with published weights; the report includes each grade and its evidence. These are workload-specific engineering proxies, not universal rankings.

In the current benchmark workload (the sample project used by benchmarks), func has a mean cold start of 40.13 ms and a raw artifact size of 45.02 KiB. Its startup performance is on par with Commander and cac, while its artifact is substantially smaller than those produced by yargs and oclif—placing it in the top tier for both performance and size. In the same report’s proxy evaluation of developer experience and maintainability, func scores 86 and 83 respectively, the highest scores in this comparison.

Category Performance Score
Performance func registers every command in advance through reflection, keeping execution static and complexity low.
Size The func framework itself has a small footprint and includes validation, composition, and common parsers.
Developer experience Complete type support and editor assistance, backed by thoughtful project architecture and scaffolding.

The following examples implement the same input rules. func keeps types, defaults, and validation on the fields they describe, and the handler receives input only after it has been converted and validated.

Comparing the same command

The same input rules implement the artifact inspect command: a required reference, a platform enum, a numeric retry count, and a JSON flag. Line counts exclude imports and shared business functions.

Commander 34 lines
const artifact = program
  .command('artifact')
  .description('inspect an artifact')

artifact
  .command('inspect')
  .requiredOption('--reference <image>')
  .addOption(
    new Option('--platform <platform>')
      .choices(platforms)
      .default('linux/amd64'),
  )
  .option(
    '--retries <count>',
    'download retries',
    value => {
      const retries = Number(value)
      if (Number.isNaN(retries)) {
        throw new InvalidArgumentError(
          'retries must be a number',
        )
      }
      return retries
    },
    2,
  )
  .option('--json')
  .action(options => {
    if (!isDigestReference(options.reference)) {
      throw new Error(
        'reference must include a sha256 digest',
      )
    }

    inspectArtifact(options.reference, options)
  })
Parser callbacks, defaults, and error branches accumulate on the command chain.
func 18 lines
@Command({ name: 'artifact' })
class ArtifactCommand extends FuncCommand {
  @Required()
  @ValueValidate(isDigestReference)
  @Value()
  reference?: string

  @Enum(platforms)
  @Value()
  platform = 'linux/amd64'

  @Value({ type: Number })
  retries = 2

  @Flag()
  json = false

  @Handler({ path: ['inspect'] })
  inspect(): void {
    inspectArtifact(this.reference!, this)
  }
}
Default parsers and field validators prepare input; inspect only calls the business function.

Lines of code are not a criterion for evaluating a framework. These examples simply show that func remains fast and lightweight while using clearer, more modern, and developer-friendly engineering practices. This keeps a project healthy, easy to understand at a glance, and ready to extend or maintain at any time.

Agent support

Beyond the benefits above, func also provides excellent support for agents.

Type safety is central to func. Clear, rigorous interfaces establish firm boundaries between commands, input, validation, handlers, and services. This allows an agent to follow project rules and generate safe, reliable, stable, and well-structured code that conforms to the architecture. When necessary, you may not need to write code yourself at all—guiding the business logic can be enough to produce a high-quality terminal tool.

At the same time, func provides complete testing support and agent compatibility. An agent can run commands from a user’s perspective, verify exit behavior and stable output, and add automated acceptance tests for expected behavior—further safeguarding business logic and project quality through automation.

Open the Agent guide. Choose a task that fits the project’s current stage, then let an agent create the project, improve its structure, or add CLI behavior tests.

Where to start next

Choose a documentation entry based on your current goal: