@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.
Additional data type options
Section titled “Additional data type options”All types (except enum) can be used without any arguments, but most take optional arguments that further narrow the type’s behavior.
# @type=stringNO_ARGS=# @type=string(minLength=5, maxLength=10, toUpperCase=true)WITH_ARGS=Dynamic type options
Section titled “Dynamic type options”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:3000One 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=localSince membership is only known at resolution time, generated code types a dynamic enum as a plain string, and members must resolve to strings.
Coercion & validation process
Section titled “Coercion & validation process”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)
Default behavior
Section titled “Default behavior”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=fooINFERRED_NUMBER=123 # infers number typeQUOTED_NUM_STRING="123" # remains a string unless @type=number is usedINFERRED_BOOLEAN=true
# return type of some functions can be inferredCONCAT_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 stringDEFAULTS_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.
Built-in data types
Section titled “Built-in data types”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).
string
Section titled “string”Options:
minLength(number): Minimum length of the stringmaxLength(number): Maximum length of the stringisLength(number): Exact length requiredstartsWith(string): Required starting substringendsWith(string): Required ending substringmatches(string|RegExp): Regular expression pattern to match. Use/pattern/flagssyntax or a quoted string pattern (see regex-like strings)toUpperCase(boolean): Convert to uppercasetoLowerCase(boolean): Convert to lowercaseallowEmpty(boolean): Allow empty string (default: false)
# @type=string(minLength=5, maxLength=10, toUpperCase=true)MY_STRING=valuenumber
Section titled “number”Options:
min(number): Minimum allowed value (inclusive)max(number): Maximum allowed value (inclusive)coerceToMinMaxRange(boolean): Coerce value to be withinmin/maxrangeisDivisibleBy(number): Value must be divisible by this numberisInt(boolean): Value must be an integer (equivalent toprecision=0)precision(number): Number of decimal places to keep
# @type=number(min=0, max=100, precision=1)MY_NUMBER=42.5boolean
Section titled “boolean”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=booleanMY_BOOL=trueOptions:
prependHttps(boolean): Automatically prepend “https://” if no protocol is specifiedallowedDomains(string[]): List of allowed domainsnoTrailingSlash(boolean): Disallow a trailing slash on the URL path (except root/)matches(string|RegExp): Regular expression pattern the full URL must match. Use/pattern/flagssyntax 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/v1Checks 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=developmentOptions:
normalize(boolean): Convert email to lowercase
# @type=email(normalize=true)MY_EMAIL=User@Example.comChecks 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=3000Checks for a valid IP address.
Options:
version(4|6): IPv4 or IPv6normalize(boolean): Convert to lowercase
# @type=ip(version=4, normalize=true)MY_IP=192.168.1.1semver
Section titled “semver”Checks for a valid semantic version.
# @type=semverMY_VERSION=1.2.3-beta.1isoDate
Section titled “isoDate”Checks for valid ISO 8601 date strings with optional time and milliseconds.
# @type=isoDateMY_DATE=2024-03-20T15:30:00ZChecks for valid UUID (versions 1-5 per RFC4122, including NIL).
# @type=uuidMY_UUID=123e4567-e89b-12d3-a456-426614174000Checks for valid MD5 hash.
# @type=md5MY_HASH=d41d8cd98f00b204e9800998ecf8427esimple-object
Section titled “simple-object”Validates and coerces JSON strings into objects. Equivalent to a bare record; prefer record, which can also validate keys and values.
# @type=simple-objectMY_OBJECT={"key": "value"}duration
Section titled “duration”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, orweeksmin/max: bounds in any duration format (e.g.min="1s",max="1d")
# Default: output is milliseconds# @type=durationREQUEST_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=15mSame 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 forprocess.envformat(separator|json, defaultseparator): how the value serializes back intoprocess.env.jsonemits a JSON array string. Arrays of objects/arrays always use JSONminLength/maxLength/isLength(number): element count bounds (or an exact count).minLengthdefaults to 1, so an explicitly-empty[]is invalid unless you setminLength=0(an explicitisLengthalso 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.
record
Section titled “record”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)orkeyType=string(matches="[a-z]+")entriesMinLength/entriesMaxLength/entriesIsLength(number): entry count bounds (or an exact count).entriesMinLengthdefaults to 1, so an explicitly-empty{}is invalid unless you setentriesMinLength=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.