@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:
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
URLobject. - If the transform throws,
funccreates anF_RUNTIME_VALIDATIONerror, preserves the original error as itscause, 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():
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/releasesDo 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.