Decorators
Bind a class property to an environment variable with the @Envapt decorator. Modern TC39 accessor decorators by default, legacy decorators at envapt/legacy.
@Envapt binds a class property to an environment variable. It's the class-based counterpart to the Envapter readers, the same parsing, converters, and cache, declared as typed fields instead of method calls.
The default import is a TC39 Stage 3 accessor decorator. Declare the field with the accessor keyword.
class {
@('PORT', { : ., : 3000 })
static accessor : number;
@('DEBUG', { : ., : false })
static accessor : boolean;
}
.port;The value is read and converted once, on first access, then cached.
Declaring decorated fields
The right form depends on whether the field is static or instance.
- Static fields use
static accessor x: T. - Instance fields use
accessor x!: T, with the!for definite assignment.
No readonly, no declare, no initializer, and no experimentalDecorators flag. This form runs on any runtime that understands Stage 3 decorators, including Bun and Deno executing a .ts file directly.
The accessor is read-only. It resolves from the environment, and assigning to it throws EnvaptError.
Legacy decorators
If your project already runs on experimentalDecorators, import the legacy decorators from envapt/legacy. They keep the property-decorator form, a getter installed with Object.defineProperty.
- Static fields use a plain
static readonly x: T, with nodeclareand no initializer. - Instance fields use
declare readonly x: T, with no initializer.
import { Envapt, Converters } from 'envapt/legacy';
class Config {
@Envapt('PORT', { converter: Converters.Number, fallback: 3000 })
static readonly port: number;
@Envapt('SESSION_TTL', { converter: Converters.Time, fallback: '15m' })
declare readonly sessionTtl: number;
}Two compiler behaviors decide where the getter sits. For an instance field, useDefineForClassFields (on by default for modern targets) emits a per-instance assignment in the constructor that would overwrite the getter with undefined, so declare makes the field type-only and the getter survives. For a static field the reverse holds. Under TypeScript 6's tsc a declare static member is erased and the decorator is applied to the prototype, so the static read never reaches the getter. TypeScript 7's compiler applies it to the constructor and the read resolves. A plain static readonly keeps the decorator on the constructor under both compilers.
The legacy form needs experimentalDecorators enabled.
{
"compilerOptions": {
"experimentalDecorators": true
}
}A module that imports decorators only from envapt/legacy must also import 'envapt' once so the runtime source binds. Runtime-specific notes (Bun running a .ts entry directly, Deno's flag) are in Compatibility.
Converters
The converter option takes the same values as getUsing/getWith, a built-in token, a primitive constructor, a custom function, or Converters.array(...). A fallback removes undefined from the value and must match the converter's type.
class {
// duration token: a time-string fallback is coerced to milliseconds
@('CACHE_TTL', { : ., : '15m' })
static accessor : number;
// array token
@('ALLOWED_ORIGINS', { : .({ : . }), : [] })
static accessor : string[];
// custom function: returns a shape the array builder can't, here a deduped Set
@('TRUSTED_IPS', { : () => new (( ?? '').(',').()) })
static accessor : <string>;
}With no converter and no fallback, the property resolves to the raw string or undefined.
The field type is checked
The decorated field's declared type must hold the converter's output, or tsc rejects it. The field can be wider than the output, never narrower.
class {
// with a fallback, the output is number
@('PORT', 3000)
static accessor : number;
// without a fallback the value can be missing, so the output is number | undefined
@('TIMEOUT')
static accessor : number | undefined;
}A no-fallback read makes the output T | undefined, and fallback: undefined counts the same. required: true makes it T. A field that cannot hold the output fails to compile with [envapt] field type must hold the converter output. The same check applies to the legacy decorators.
Shorthand decorators
@EnvNum, @EnvStr, @EnvBool, @EnvUrl, and @EnvTime are shorthands for @Envapt with a fixed converter. The call site is the key and an optional fallback.
class {
@('PORT', 3000)
static accessor : number;
@('AWS_REGION', 'us-east-1')
static accessor : string;
@('DEBUG', false)
static accessor : boolean;
@('APP_URL', new ('http://localhost:3000'))
static accessor : URL;
@('CACHE_TTL', '15m')
static accessor : number;
}The second argument is the fallback, typed to the converter, @EnvUrl takes a URL, @EnvTime takes a millisecond number or a time string, the rest take their primitive. Omit it and the property resolves to the converted value or undefined. The key can also be an ordered list.
These are accessor decorators like @Envapt, so the same declaration rule applies. They take a fallback only. For required, a schema, an array, or a custom function, use @Envapt with the options object.
Required values
Pass required: true to throw EnvaptError on a missing or empty value (or, under strict mode, a whitespace-only value) instead of returning a fallback. It's mutually exclusive with fallback.
class {
@('DATABASE_URL', { : ., : true })
static accessor : URL;
}The throw happens on first access to the property. An unset or empty value throws in any mode. A whitespace-only value throws only under strict mode. See Strict mode for the global switch and how required relates to it.
First access can be deep in a request, not at boot. A missing required value then surfaces as a runtime error mid-request. Read the property once during startup to fail early.
Validation with a schema
Pass a schema to validate through zod, valibot, arktype, or a hand-rolled StandardSchemaV1. The property type is the schema's output.
class {
@('STRIPE_SECRET_KEY', { : .().('sk_') })
static accessor : string;
}schema and converter are mutually exclusive. See Standard Schema for the full behavior.
Ordered keys
Like the functional readers, the key can be an ordered list. The first key with a value wins.
class {
// CANARY_URL if set, otherwise APP_URL
@(['CANARY_URL', 'APP_URL'], { : . })
static accessor : URL | undefined;
}Static and instance properties
@Envapt works on both static and instance properties, and the class does not need to extend anything. The cache is keyed per class and property, so a static and an instance property of the same name never collide.
Extend Envapter when you also want the reader methods on the same class:
class extends {
@('PORT', { : ., : 3000 })
accessor !: number;
// mix decorator fields with reader calls
readonly = this.('AWS_REGION', 'us-east-1');
}
const = new ();
.port;A misconfigured options object throws EnvaptError as the decorator is applied, before any property access, required together with a fallback, or a non-StandardSchemaV1 value passed as schema. See Errors for the codes.