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
-
Use
as constfor enums to get literal types:NODE_ENV: schema.enum(['dev', 'prod'] as const) // 'dev' | 'prod'
NODE_ENV: schema.enum(['dev', 'prod']) // string -
Annotate custom validator return types for complex transformations:
parsed: schema.custom((value): MyComplexType => {
return JSON.parse(value) as MyComplexType;
}) -
Use optional chaining for optional nested objects:
const port = env.get('config')?.database?.port; // Safe access -
Extract types for reuse across your application:
type DatabaseConfig = ReturnType<typeof env.get<'database'>>;
type ServerConfig = ReturnType<typeof env.get<'server'>>;