Skip to content

@type data types

The @type item decorator sets the data type associated with an item. The data type affects coercion, validation, and generated type files.

All types (except enum) can be used without any arguments, but most take optional arguments that further narrow the type’s behavior.

# @type=string
NO_ARGS=
# @type=string(minLength=5, maxLength=10, toUpperCase=true)
WITH_ARGS=

Type option values can be resolver functions instead of static values, so validation can vary per environment or based on other items:

# @type=enum(dev, production)
APP_ENV=dev
# stricter length requirement in production
# @type=string(minLength=if(eq($APP_ENV, production), 32, 8))
API_TOKEN=
# require at least one entry only in production
# @type=array(email, minLength=if(eq($APP_ENV, production), 1, 0))
ALERT_EMAILS=[]

The whole type can also be dynamic, resolving to a type name:

# validate as a url in production, allow anything in dev
# @type=if(eq($APP_ENV, production), url, string)
SERVICE_HOST=localhost:3000

One constraint: dynamic parts may vary validation behavior, but not the generated types (generated code must not differ per environment). All possible types must generate the same type. A url and a string both generate a string, so switching between them is fine; switching between number and string is a schema error. For generated code, the first static candidate in the expression is used (url above). The same rule applies to options that affect generated types (e.g. number(isInt=...) must stay static).

Dynamic whole types resolve to a bare type name; to combine a dynamic type with options, put the dynamic part in the option instead (e.g. string(minLength=if(...))).

Enum members can also be sourced from other items. A referenced array spreads its elements into the member list:

# @type=array(string)
ALLOWED_MODES=[dev, staging, prod]
# value must be one of the elements of ALLOWED_MODES
# @type=enum($ALLOWED_MODES)
APP_MODE=dev
# static members and references can be mixed
# @type=enum(local, $ALLOWED_MODES)
BUILD_MODE=local

Since membership is only known at resolution time, generated code types a dynamic enum as a plain string, and members must resolve to strings.

Once a raw value is resolved - which could from a static value in an .env file, a function, or an override passed into the process - the raw value will be coerced and validated based on the type, respecting additional arguments provided to the type.

Consider the following example:

# @type=number(precision=0, max=100)
ITEM="123.45"

The internal coercion/validation process looks like:
"123.45" -> 123.45 -> 123 -> ❌ invalid (greater than max)

When no @type is specified, a type will be inferred where possible - for static values, and some functions that return a known type. Note that the use of quotes matters. Otherwise the type will default to string.

INFERRED_STRING_QUOTED="foo"
INFERRED_STRING_UNQUOTED=foo
INFERRED_NUMBER=123 # infers number type
QUOTED_NUM_STRING="123" # remains a string unless @type=number is used
INFERRED_BOOLEAN=true
# return type of some functions can be inferred
CONCAT_INFERS_STRING=`concat-${SOMEVAR}-will-be-string`
FN_INFER_BOOLEAN=eq($VAR1, $VAR2)
DEFAULTS_TO_STRING_FN=fnThatCannotInferType()
# with no other info, we default to string
DEFAULTS_TO_STRING=

Note that numeric values that would lose precision, or change any formatting (like leading/trailing zeros), will be treated as strings unless explicitly adding @type=number.

In any slightly ambiguous situation, it is better to explicitly add a @type decorator.

These are the built-in data types. Plugins may register additional data types, which appear in generated code according to the coerced type they declare (as strings if they don’t declare one).

Options:

  • minLength (number): Minimum length of the string
  • maxLength (number): Maximum length of the string
  • isLength (number): Exact length required
  • startsWith (string): Required starting substring
  • endsWith (string): Required ending substring
  • matches (string|RegExp): Regular expression pattern to match. Use /pattern/flags syntax or a quoted string pattern (see regex-like strings)
  • toUpperCase (boolean): Convert to uppercase
  • toLowerCase (boolean): Convert to lowercase
  • allowEmpty (boolean): Allow empty string (default: false)
# @type=string(minLength=5, maxLength=10, toUpperCase=true)
MY_STRING=value

Options:

  • min (number): Minimum allowed value (inclusive)
  • max (number): Maximum allowed value (inclusive)
  • coerceToMinMaxRange (boolean): Coerce value to be within min/max range
  • isDivisibleBy (number): Value must be divisible by this number
  • isInt (boolean): Value must be an integer (equivalent to precision=0)
  • precision (number): Number of decimal places to keep
# @type=number(min=0, max=100, precision=1)
MY_NUMBER=42.5

The following values will be coerced to a boolean and considered valid:

  • True values: "t", "true", true, "yes", "on", "1", 1
  • False values: "f", "false", false, "no", "off", "0", 0

Anything else will be considered invalid.

# @type=boolean
MY_BOOL=true

Options:

  • prependHttps (boolean): Automatically prepend “https://” if no protocol is specified
  • allowedDomains (string[]): List of allowed domains
  • noTrailingSlash (boolean): Disallow a trailing slash on the URL path (except root /)
  • matches (string|RegExp): Regular expression pattern the full URL must match. Use /pattern/flags syntax or a quoted string pattern (see regex-like strings)
# @type=url(prependHttps=true)
MY_URL=example.com/foobar
# @type=url(noTrailingSlash=true, matches=/^https:\/\/api\./)
API_URL=https://api.example.com/v1

Checks a value is contained in a list of possible values - it must match one exactly. Members can also be sourced from other items (see Dynamic type options).

NOTE - this is the only type that cannot be used without any additional arguments

# @type=enum(development, staging, production)
ENV=development

Options:

  • normalize (boolean): Convert email to lowercase
# @type=email(normalize=true)
MY_EMAIL=User@Example.com

Checks for valid port number. Coerces to a number.

Options:

  • min (number): Minimum port number (default: 0)
  • max (number): Maximum port number (default: 65535)
# @type=port(min=1024, max=9999)
MY_PORT=3000

Checks for a valid IP address.

Options:

  • version (4|6): IPv4 or IPv6
  • normalize (boolean): Convert to lowercase
# @type=ip(version=4, normalize=true)
MY_IP=192.168.1.1

Checks for a valid semantic version.

# @type=semver
MY_VERSION=1.2.3-beta.1

Checks for valid ISO 8601 date strings with optional time and milliseconds.

# @type=isoDate
MY_DATE=2024-03-20T15:30:00Z

Checks for valid UUID (versions 1-5 per RFC4122, including NIL).

# @type=uuid
MY_UUID=123e4567-e89b-12d3-a456-426614174000

Checks for valid MD5 hash.

# @type=md5
MY_HASH=d41d8cd98f00b204e9800998ecf8427e

Validates and coerces JSON strings into objects. Equivalent to a bare record; prefer record, which can also validate keys and values.

# @type=simple-object
MY_OBJECT={"key": "value"}

Flexible duration type. Accepts human-readable strings ("1h", "30m", "500ms", "2days") or bare numbers (interpreted as milliseconds, plain decimals only, no hex/exponent/Infinity notation), and outputs a number in the unit you specify.

Options:

  • output: output unit: ms (default), seconds, minutes, hours, days, or weeks
  • min / max: bounds in any duration format (e.g. min="1s", max="1d")
# Default: output is milliseconds
# @type=duration
REQUEST_TIMEOUT=30s
# Output in seconds, typical for HTTP client configs
# @type=duration(output="seconds")
HTTP_TIMEOUT=1h
# With min/max bounds
# @type=duration(output="minutes", min="1m", max="1d")
POLL_INTERVAL=15m

Same parser is used by cache(..., ttl=...) and the plugin cacheTtl option, so any string that works there also works here. For cache mode behavior and troubleshooting, see the Caching guide.

A list of values, each coerced and validated with an element type.

Element type is the first positional argument: a type name (array(email)) or a nested type call for types that take their own options or positional args (array(email(normalize=true)), array(enum(dev, staging, prod))). Omitting it defaults to string elements.

Array options:

  • separator (string, default ","): splits plain-string input (e.g. a real environment variable override) and joins the value back into a string for process.env
  • format (separator | json, default separator): how the value serializes back into process.env. json emits a JSON array string. Arrays of objects/arrays always use JSON
  • minLength / maxLength / isLength (number): element count bounds (or an exact count). minLength defaults to 1, so an explicitly-empty [] is invalid unless you set minLength=0 (an explicit isLength also overrides the default)
  • unique (boolean): reject duplicate elements

Values can be written three ways:

# native literal - elements support refs and function calls
# @type=array(email)
ALLOWED_EMAILS=[admin@example.com, ${SUPPORT_EMAIL}]
# separator-joined string - how a real env var override arrives
# @type=array(url, separator=";")
SERVICE_URLS="https://a.example.com;https://b.example.com"
# JSON array string
# @type=array(number)
SCORES='[10, 20, 30]'

Validation errors are reported per element ([1] Current value is not in list of possible values).

A scalar element that contains the separator fails validation (it could not round-trip through process.env); set format=json or pick a different separator.

In application code, ENV.ALLOWED_EMAILS is a real typed array (string[], number[], etc. via type generation). In process.env the value is the flat string form (separator-joined, or JSON when format=json).

An empty or whitespace-only string input resolves to a missing value (undefined), never an empty array; the only way to express an empty array is the explicit [] literal (sanctioned via minLength=0).

An untyped item whose value is an array literal (e.g. ITEM=[a, b]) is inferred as an array automatically.

An object (a keyed record) whose values are coerced and validated with a value type, with optional key validation. Named record (matching TS Record and zod convention) since it types ALL values uniformly rather than specific keys.

Value type is the first positional argument, same forms as array. Bare @type=record accepts any object without per-value validation.

Object options:

  • keyType: a type used to validate every key, e.g. keyType=enum(us, eu) or keyType=string(matches="[a-z]+")
  • entriesMinLength / entriesMaxLength / entriesIsLength (number): entry count bounds (or an exact count). entriesMinLength defaults to 1, so an explicitly-empty {} is invalid unless you set entriesMinLength=0
# every value must be a valid url
# @type=record(url)
ENDPOINTS={api=https://api.example.com, docs=https://docs.example.com}
# keys restricted to an enum, values validated as numbers
# @type=record(number, keyType=enum(us, eu))
REGION_LIMITS={us=100, eu=50}
# JSON object strings also work
# @type=record(number)
LIMITS='{"low": 1, "high": 100}'

Validation errors are reported per entry ("apac" is not a valid key - ...).

In application code, ENV.ENDPOINTS is typed as Record<string, string> (enum-constrained keys narrow further). In process.env the value is JSON.

An untyped item whose value is an object literal (e.g. ITEM={k=v}) is inferred as a record automatically. The existing simple-object type behaves the same as a bare record.