中文

Custom decorators

Use createValueDecorator to turn one string input into a URL or another domain value.

Updated

@Value() is the common extractor for built-in strings and finite numbers. Applications often have domain-specific input rules of their own. createValueDecorator() moves those rules to a reusable option boundary, avoiding repetitive conversion code in every Command.

Create a URL decorator

createValueDecorator() accepts a synchronous transform. func always passes it one raw string and assigns its return value to the decorated field:

src/decorators/url.decorator.ts
import { createValueDecorator } from 'func'

export const Url = createValueDecorator(input => {
  if (!URL.canParse(input)) throw new Error('Expected a valid absolute URL.')

  const url = new URL(input)
  if (url.protocol !== 'http:' && url.protocol !== 'https:') {
    throw new Error('Expected an HTTP or HTTPS URL.')
  }

  return url
})
  • It declares a single-value CLI option and converts a valid HTTP(S) string into a standard URL object.
  • If the transform throws, func creates an F_RUNTIME_VALIDATION error, preserves the original error as its cause, and does not invoke the Handler.

Use it in a command

A custom decorator accepts the same name, alias, and description input as @Value(). It can also compose with @Required(), @Deprecated(), @DependsOn(), @Exclusive(), and @ValueValidate():

src/commands/deploy.command.ts
import { Command, Handler, Required } from 'func'
import { Url } from '../decorators/url.decorator.js'

@Command('deploy')
export class DeployCommand {
  @Required()
  @Url({ alias: 'u', description: 'Deployment endpoint' })
  endpoint!: URL

  @Handler()
  run() {
    console.log(`Deploying to ${this.endpoint.origin}`)
  }
}

Both forms below produce a URL instance:

ship deploy --endpoint https://api.example.com/releases
ship deploy -u https://api.example.com/releases

Do not add @Value() to the same field. @Url() already declares the single-value option.

Transform timing and defaults

The transform runs once only when the user explicitly supplies the option. When it is omitted, func preserves the field initializer or undefined, so a default must already use the output type:

@Url()
endpoint = new URL('https://api.example.com')

Do not use a raw string as the default because func does not transform defaults. @Required() and field validators run after transformation, so @ValueValidate() receives the URL, not the raw string.

Transforms must be synchronous and should be pure. Put network access, filesystem work, and other side effects in a service or handler. Returning a Promise creates a validation error.

Type boundary

The generic output is inferred from the transform, but an ordinary TypeScript property decorator cannot prove that a field declaration matches it. If a decorator returns a URL, consumers should explicitly declare the field as URL.

Code that reads the merged transformed value through @Args() can specify its option type:

import { Args } from 'func'
import type { Args as ArgsValue } from 'func'

run(@Args() args: ArgsValue<{ endpoint: URL }>) {
  console.log(args.options.endpoint.origin)
}

Both parameter forms remain available: @Url('endpoint') and @Url({ name: 'endpoint', alias: 'u' }). Help still presents it as a value option that consumes one string token; the decorator decides what that string becomes.