---
title: Field Types
description: The 10 built-in field type builders for defining environment variable contracts — from strings and numbers to URLs and durations, each with chainable validation methods.
url: https://pr-1-8289d63b6330.thally.app/envlock/field-types
---

# Field Types

The 10 built-in field type builders for defining environment variable contracts — from strings and numbers to URLs and durations, each with chainable validation methods.

Envlock provides 10 field type builders through the `env` namespace. Each builder returns an immutable `Field` object that describes how to parse, validate, and document an environment variable. Fields use a chainable pattern — every method returns a new frozen field with the modifications applied.

## The builder pattern

Each field builder creates a required field by default:

```ts
env.string()      // Field<string, true>
env.number()      // Field<number, true>
env.port()        // Field<number, true>
env.boolean()     // Field<boolean, true>
```

Call chain methods to modify the field — each returns a new immutable field:

```ts
env.port()
  .default(3000)
  .describe("HTTP listen port")
  .example("8080")
```

## Field builders

### env.string()

Parses the variable as a verbatim string with no trimming or transformation.

```ts
import { env } from "@envlock/core";

const field = env.string();
// Accepts any non-empty string, including whitespace
```

No constraints are applied — any non-empty value is valid. Use this for API keys, secrets, or other opaque strings where trimming might matter.

### env.number()

Parses the variable as a finite number. Input is trimmed; blank values, `NaN`, and `Infinity` are rejected.

```ts
const field = env.number();
// Valid: "42", "3.14", " -7.5 "
// Invalid: "", "NaN", "Infinity", "abc"
```

Constraint string: `"finite number"`

### env.integer()

Parses the variable as a whole number within the safe integer range (`Number.MIN_SAFE_INTEGER` to `Number.MAX_SAFE_INTEGER`).

```ts
const field = env.integer();
// Valid: "42", "-7", " 0 "
// Invalid: "3.14", "99999999999999999999" (beyond safe range)
```

Constraint string: `"whole number"`

### env.boolean()

Parses case-insensitive boolean strings. Accepts:
- `true`, `false`
- `1`, `0`
- `yes`, `no`
- `on`, `off`

```ts
const field = env.boolean();
// Valid: "true", "TRUE", "1", "yes", "NO", "off"
// Invalid: "maybe", "enabled", "yes!"
```

Constraint string: `"true/false, 1/0, yes/no, on/off"`

### env.port()

Parses the variable as an integer in the range 1 to 65535 (valid TCP/UDP port numbers).

```ts
const field = env.port();
// Valid: "80", "3000", "65535"
// Invalid: "0", "65536", "3000.5"
```

Constraint string: `"integer 1-65535"`

### env.url()

Validates the variable as an absolute URL using the `URL` API. Optionally restrict to specific protocols.

```ts
const field = env.url();
// Valid: "https://example.com", "postgres://localhost"

const restricted = env.url({ protocols: ["https:", "postgres:"] });
// Valid: "https://example.com", "postgres://user@localhost/db"
// Invalid: "http://example.com", "ftp://example.com"
```

Pass `UrlOptions` to restrict which protocols are allowed. The constraint string changes based on options:
- Without protocol restriction: `"absolute URL"`
- With `protocols: ["https:"]`: `"absolute URL with protocol https:"`
- With multiple protocols: `"absolute URL with protocol https: or postgres:"`

### env.enum()

Restricts the variable to one of a fixed set of string values. The type is a literal union matching your choices.

```ts
const field = env.enum(["development", "test", "production"]);
// Valid: "development", "test", "production"
// Invalid: "dev", "prod", "staging"

// Type: "development" | "test" | "production"
```

Constraint string: `"one of: development, test, production"`

### env.json()

Parses the variable using `JSON.parse`. You can specify the expected type with a generic parameter.

```ts
const field = env.json<{ debug: boolean; timeout: number }>();
// Valid: '{"debug":true,"timeout":30}'
// Invalid: "not json", "undefined", "{debug: true}" (unquoted keys)

const untyped = env.json();
// Type: unknown
```

Constraint string: `"JSON document"`

### env.duration()

Parses duration strings in human-readable format, returning the value in milliseconds. Supported formats:

- `250ms` — milliseconds
- `30s` — seconds
- `5m` — minutes
- `2h` — hours
- `1d` — days
- Bare number — interpreted as milliseconds

```ts
const field = env.duration();
// Valid: "250ms", "30s", "5m", "2h", "1d", "1000"
// Invalid: "30s30ms", "1w" (weeks not supported)

// All return the value in milliseconds
// "30s" → 30000
// "2h" → 7200000
```

Constraint string: `"duration like 30s, 5m, 2h, stored as milliseconds"`

### env.list()

Parses a comma-separated list of values. Items are trimmed and empty items are dropped.

```ts
const field = env.list();
// "a, b, c" → ["a", "b", "c"]
// " x , , y " → ["x", "y"]

const custom = env.list({ separator: ";" });
// "a;b;c" → ["a", "b", "c"]
```

Constraint string: `"comma-separated list"` (or your custom separator in the message)

## Chain methods

After creating a field, call these methods to customize it. Each returns a new frozen field with your modifications:

### .optional()

Marks the field as optional. Absent values (undefined or empty string) produce no validation issue.

```ts
const field = env.string().optional();
// If not set: no issue, field is omitted from result
// If set: parsed and included in result

// Type changes: Field<string, false>
```

When a field is optional, its property in the parsed result is optional (`DEBUG?: boolean`).

### .default(value)

Provides a default value when the variable is absent. This also marks the field as required in the final type.

```ts
const field = env.port().default(3000);
// If not set: returns 3000
// If set: parsed normally

// Type: Field<number, true>
```

The default value must match the field's type. Calling `.default()` clears `.optional()` — a field cannot be both optional and have a default.

### .secret()

Marks the value as sensitive. Secret values are:
- Masked as `"••••••"` in validation error messages
- Hidden in `.env.example` output (shown as `KEY=`)
- Redacted when printing configs

```ts
const field = env.string().secret();
const secretDefault = env.string().default("dev-key").secret();
```

Use this for API keys, database passwords, tokens, and other credentials.

### .describe(text)

Adds a human-readable description of the variable's purpose. Descriptions appear in:
- `.env.example` files
- `envlock inspect` output
- Validation error messages

```ts
const field = env.port()
  .describe("HTTP server listen port");
```

Keep descriptions concise but clear about what the value controls.

### .example(text)

Sets an example value to show in `.env.example`. The example is not validated — it's purely for documentation.

```ts
const field = env.url({ protocols: ["postgres:"] })
  .example("postgres://user:pass@localhost/mydb");
```

For secrets, `.example()` is hidden; only the key name appears in `.env.example`.

## Comprehensive example

Here's a real-world config file using multiple field types and chain methods:

```ts
import { defineEnv, env } from "@envlock/core";

export default defineEnv({
  NODE_ENV: env
    .enum(["development", "staging", "production"])
    .default("development")
    .describe("Runtime environment for feature flags and logging"),

  PORT: env
    .port()
    .default(3000)
    .describe("HTTP server listen port"),

  DATABASE_URL: env
    .url({ protocols: ["postgres:", "postgresql:"] })
    .secret()
    .describe("Primary database connection string"),

  DATABASE_TIMEOUT: env
    .duration()
    .default(30000)
    .describe("Query timeout in milliseconds"),

  REDIS_URL: env
    .url({ protocols: ["redis:", "rediss:"] })
    .optional()
    .describe("Optional Redis cache URL"),

  API_KEY: env
    .string()
    .secret()
    .describe("Third-party API key for rate-limited service"),

  LOG_LEVEL: env
    .enum(["debug", "info", "warn", "error"])
    .default("info")
    .describe("Minimum log level to output"),

  MAX_UPLOAD_SIZE: env
    .number()
    .default(52428800)
    .describe("Maximum file upload size in bytes"),

  ALLOWED_ORIGINS: env
    .list({ separator: "," })
    .default(["http://localhost:3000"])
    .describe("Comma-separated list of allowed CORS origins"),

  FEATURE_FLAGS: env
    .json<{ beta: boolean; newUI: boolean }>()
    .optional()
    .describe("Feature flags as JSON object"),

  WORKER_THREADS: env
    .integer()
    .default(4)
    .describe("Number of worker threads for task processing"),
});
```

In your code, load this schema and access the typed values:

```ts
import { loadEnv, redact } from "@envlock/core";
import schema from "./envlock.config.mjs";

const config = loadEnv(schema);
// config: {
//   NODE_ENV: "development" | "staging" | "production",
//   PORT: number,
//   DATABASE_URL: string,
//   DATABASE_TIMEOUT: number,
//   REDIS_URL?: string,
//   API_KEY: string,
//   LOG_LEVEL: "debug" | "info" | "warn" | "error",
//   MAX_UPLOAD_SIZE: number,
//   ALLOWED_ORIGINS: string[],
//   FEATURE_FLAGS?: { beta: boolean; newUI: boolean },
//   WORKER_THREADS: number,
// }

// Log config safely (secrets are masked)
console.log(redact(config, schema));
```