Skip to main content

Type Inference

Envitron provides full TypeScript type inference for all schema types, including deeply nested objects and typed arrays. This means you get autocomplete and type checking throughout your codebase.

Basic Type Inference

All primitive types are automatically inferred:

const env = createEnvSchema((schema) => ({
PORT: schema.number(),
HOST: schema.string(),
DEBUG: schema.boolean(),
NODE_ENV: schema.enum(['development', 'production'] as const),
}));

const port = env.get('PORT'); // Type: number
const host = env.get('HOST'); // Type: string
const debug = env.get('DEBUG'); // Type: boolean
const nodeEnv = env.get('NODE_ENV'); // Type: 'development' | 'production'

Nested Object Type Inference

Types are fully inferred for nested objects:

const env = createEnvSchema((schema) => ({
database: schema.object({
host: schema.string(),
port: schema.number(),
credentials: schema.object({
username: schema.string(),
password: schema.string(),
}),
}),
}));

// Full type inference and autocomplete!
const db = env.get('database');
// Type: { host: string; port: number; credentials: { username: string; password: string } }

const host = db.host; // Type: string ✅
const port = db.port; // Type: number ✅
const username = db.credentials.username; // Type: string ✅

// TypeScript catches errors:
const invalid = db.nonExistent; // ❌ Error: Property 'nonExistent' does not exist

Typed Array Inference

Arrays with element validators are fully typed:

const env = createEnvSchema((schema) => ({
ports: schema.array(schema.number()),
tags: schema.array(schema.string()),
flags: schema.array(schema.boolean()),
}));

const ports = env.get('ports'); // Type: number[]
const tags = env.get('tags'); // Type: string[]
const flags = env.get('flags'); // Type: boolean[]

// TypeScript knows the element types:
const firstPort = ports[0]; // Type: number ✅
const firstTag = tags[0]; // Type: string ✅

Arrays of Objects

Complex arrays maintain full type information:

const env = createEnvSchema((schema) => ({
users: schema.array(
schema.object({
name: schema.string(),
age: schema.number(),
active: schema.boolean(),
})
),
}));

const users = env.get('users');
// Type: Array<{ name: string; age: number; active: boolean }>

// Full autocomplete on array elements:
const firstName = users[0].name; // Type: string ✅
const firstAge = users[0].age; // Type: number ✅
const firstActive = users[0].active; // Type: boolean ✅

// TypeScript catches mistakes:
const invalid = users[0].nonExistent; // ❌ Error!

Deeply Nested Structures

Type inference works at any nesting level:

const env = createEnvSchema((schema) => ({
application: schema.object({
name: schema.string(),
version: schema.string(),
services: schema.object({
api: schema.object({
url: schema.string(),
timeout: schema.number(),
retries: schema.array(schema.number()),
}),
cache: schema.object({
enabled: schema.boolean(),
ttl: schema.number(),
}),
}),
}),
}));

const app = env.get('application');
// All properties fully typed:
app.name; // Type: string
app.services.api.url; // Type: string
app.services.api.retries; // Type: number[]
app.services.cache.enabled; // Type: boolean

// TypeScript catches all errors:
app.services.api.invalidProp; // ❌ Error!
app.services.cache.ttl = "string"; // ❌ Error: Type 'string' is not assignable to type 'number'

Optional Types

Optional values are typed as T | undefined:

const env = createEnvSchema((schema) => ({
required: schema.string(),
optional: schema.string({ optional: true }),
optionalObject: schema.object(
{
host: schema.string(),
port: schema.number(),
},
{ optional: true }
),
}));

const required = env.get('required'); // Type: string
const optional = env.get('optional'); // Type: string | undefined
const obj = env.get('optionalObject'); // Type: { host: string; port: number } | undefined

// TypeScript enforces null checks:
console.log(optional.length); // ❌ Error: Object is possibly 'undefined'
console.log(optional?.length); // ✅ OK: Optional chaining
if (obj) {
console.log(obj.host); // ✅ OK: Type narrowed to non-undefined
}

Direct Property Access

Type inference works with direct property access too:

const env = createEnvSchema((schema) => ({
database: schema.object({
host: schema.string(),
port: schema.number(),
}),
}));

// Direct property access (no .get())
console.log(env.database.host); // Type: string ✅
console.log(env.database.port); // Type: number ✅

Working with all()

The all() method returns a fully typed object:

const env = createEnvSchema((schema) => ({
PORT: schema.number(),
database: schema.object({
host: schema.string(),
port: schema.number(),
}),
}));

const allEnvs = env.all();
// Type: {
// PORT: number;
// database: { host: string; port: number };
// [key: string]: any;
// }

console.log(allEnvs.PORT); // Type: number
console.log(allEnvs.database.host); // Type: string

Custom Validators

Custom validators infer types from the return value:

const env = createEnvSchema((schema) => ({
// Infers return type automatically
halfValue: schema.custom((value) => Number(value) / 2), // Type: number

uppercase: schema.custom((value) => String(value).toUpperCase()), // Type: string

parsed: schema.custom((value) => JSON.parse(value) as { id: number }), // Type: { id: number }
}));

const half = env.get('halfValue'); // Type: number
const upper = env.get('uppercase'); // Type: string
const parsed = env.get('parsed'); // Type: { id: number }

Type Safety Benefits

Autocomplete

Your IDE provides accurate autocomplete for all properties:

const db = env.get('database');
db. // IDE shows: host, port, credentials
db.credentials. // IDE shows: username, password

Refactoring Safety

Changing schema types updates all usage sites:

// Change schema:
database: schema.object({
host: schema.string(),
port: schema.string(), // Changed from number to string
})

// TypeScript will flag all places expecting number:
const port: number = env.get('database').port; // ❌ Error now!

Prevents Runtime Errors

Type checking catches mistakes before runtime:

// Typos caught at compile time:
const host = env.get('databse').host; // ❌ Error: 'databse' doesn't exist

// Wrong property access:
const invalid = env.get('database').hst; // ❌ Error: 'hst' doesn't exist

// Type mismatches:
const port: string = env.get('database').port; // ❌ Error if port is number

Real-World Example

A complete typed configuration for a microservices application:

const env = createEnvSchema((schema) => ({
server: schema.object({
port: schema.number(),
host: schema.string(),
cors: schema.object({
enabled: schema.boolean(),
origins: schema.array(schema.string()),
}),
}),
databases: schema.array(
schema.object({
name: schema.string(),
host: schema.string(),
port: schema.number(),
pool: schema.object({
min: schema.number(),
max: schema.number(),
}),
})
),
features: schema.object({
rateLimit: schema.object({
enabled: schema.boolean(),
maxRequests: schema.number(),
windowMs: schema.number(),
}),
cache: schema.object({
enabled: schema.boolean(),
ttl: schema.number(),
}),
}),
}));

// All properties are fully typed throughout your application:

// Server configuration
const serverPort = env.get('server').port; // number
const corsOrigins = env.get('server').cors.origins; // string[]

// Database configuration
const primaryDb = env.get('databases')[0]; // { name: string; host: string; port: number; pool: { min: number; max: number } }
const dbPoolMax = primaryDb.pool.max; // number

// Feature flags
const isRateLimitEnabled = env.get('features').rateLimit.enabled; // boolean
const cacheTTL = env.get('features').cache.ttl; // number

// TypeScript catches all errors at compile time:
env.get('server').invalidProp; // ❌ Error
env.get('databases')[0].pool.invalidProp; // ❌ Error
const wrongType: string = env.get('server').port; // ❌ Error

Tips for Best Type Inference

  1. Use as const for enums to get literal types:

    NODE_ENV: schema.enum(['dev', 'prod'] as const) // 'dev' | 'prod'
    NODE_ENV: schema.enum(['dev', 'prod']) // string
  2. Annotate custom validator return types for complex transformations:

    parsed: schema.custom((value): MyComplexType => {
    return JSON.parse(value) as MyComplexType;
    })
  3. Use optional chaining for optional nested objects:

    const port = env.get('config')?.database?.port; // Safe access
  4. Extract types for reuse across your application:

    type DatabaseConfig = ReturnType<typeof env.get<'database'>>;
    type ServerConfig = ReturnType<typeof env.get<'server'>>;

See Also