← Back to Home

How to Validate Configuration in TypeScript

Updated January 14, 2026
validationconfigurationschemaruntime checkszod

Validating Configuration in TypeScript

Runtime configuration validation ensures required values exist and are correctly typed.

Basic Validation Pattern

interface AppConfig {
  youtube: {
    enabled: boolean;
    clientId: string;
    clientSecret: string;
  };
  api: {
    timeout: number;
    maxRetries: number;
  };
}

function validateConfig(config: unknown): AppConfig {
  if (!config || typeof config !== 'object') {
    throw new Error('Invalid config: not an object');
  }

  const c = config as Partial<AppConfig>;

  // Validate youtube section
  if (!c.youtube || typeof c.youtube !== 'object') {
    throw new Error('Invalid config: missing youtube section');
  }

  if (typeof c.youtube.enabled !== 'boolean') {
    throw new Error('Invalid config: youtube.enabled must be boolean');
  }

  if (c.youtube.enabled) {
    if (!c.youtube.clientId || typeof c.youtube.clientId !== 'string') {
      throw new Error('Invalid config: youtube.clientId required when enabled');
    }
    if (!c.youtube.clientSecret || typeof c.youtube.clientSecret !== 'string') {
      throw new Error('Invalid config: youtube.clientSecret required when enabled');
    }
  }

  // Validate api section
  if (!c.api || typeof c.api !== 'object') {
    throw new Error('Invalid config: missing api section');
  }

  if (typeof c.api.timeout !== 'number' || c.api.timeout <= 0) {
    throw new Error('Invalid config: api.timeout must be positive number');
  }

  return config as AppConfig;
}

With Default Values

interface ConfigWithDefaults {
  api: {
    timeout: number;
    maxRetries: number;
  };
}

function loadConfigWithDefaults(userConfig: Partial<ConfigWithDefaults>): ConfigWithDefaults {
  const defaults: ConfigWithDefaults = {
    api: {
      timeout: 30000,
      maxRetries: 3,
    },
  };

  return {
    api: {
      timeout: userConfig.api?.timeout ?? defaults.api.timeout,
      maxRetries: userConfig.api?.maxRetries ?? defaults.api.maxRetries,
    },
  };
}

Using Zod for Schema Validation

import { z } from 'zod';

const ConfigSchema = z.object({
  youtube: z.object({
    enabled: z.boolean(),
    clientId: z.string().min(1),
    clientSecret: z.string().min(1),
    port: z.number().int().min(1024).max(65535).default(9671),
  }),

  api: z.object({
    timeout: z.number().positive().default(30000),
    maxRetries: z.number().int().min(0).max(10).default(3),
  }),

  features: z.object({
    useProImageModel: z.boolean().default(false),
    useProTextModel: z.boolean().default(false),
  }).optional(),
});

type AppConfig = z.infer<typeof ConfigSchema>;

function loadAndValidateConfig(configPath: string): AppConfig {
  const rawConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
  return ConfigSchema.parse(rawConfig);
}

Environment-Specific Configs

interface EnvironmentConfig {
  NODE_ENV: 'development' | 'production' | 'test';
  API_KEY: string;
  PORT: number;
}

function validateEnv(): EnvironmentConfig {
  const env = process.env;

  if (!env.NODE_ENV || !['development', 'production', 'test'].includes(env.NODE_ENV)) {
    throw new Error('Invalid NODE_ENV');
  }

  if (!env.API_KEY) {
    throw new Error('API_KEY is required');
  }

  return {
    NODE_ENV: env.NODE_ENV as EnvironmentConfig['NODE_ENV'],
    API_KEY: env.API_KEY,
    PORT: parseInt(env.PORT || '3000', 10),
  };
}

Configuration Validation Error

class ConfigValidationError extends Error {
  constructor(
    public readonly field: string,
    public readonly expected: string,
    public readonly received: unknown
  ) {
    super(`Config error: ${field} expected ${expected}, got ${typeof received}`);
    this.name = 'ConfigValidationError';
  }
}

function requireField<T>(obj: Record<string, unknown>, field: string, type: string): T {
  const value = obj[field];

  if (value === undefined || value === null) {
    throw new ConfigValidationError(field, type, 'undefined');
  }

  if (typeof value !== type) {
    throw new ConfigValidationError(field, type, typeof value);
  }

  return value as T;
}

// Usage
const config = { timeout: '30000' };
const timeout = requireField<number>(config, 'timeout', 'number');
// Throws: ConfigValidationError: timeout expected number, got string

Config File Loader

import fs from 'fs';
import path from 'path';

class ConfigManager<T> {
  private cachedConfig: T | null = null;

  constructor(
    private configPath: string,
    private validator: (config: unknown) => T
  ) {}

  load(): T {
    if (this.cachedConfig) {
      return this.cachedConfig;
    }

    if (!fs.existsSync(this.configPath)) {
      throw new Error(`Config file not found: ${this.configPath}`);
    }

    const rawContent = fs.readFileSync(this.configPath, 'utf-8');
    let rawConfig: unknown;

    try {
      rawConfig = JSON.parse(rawContent);
    } catch (error) {
      throw new Error(`Invalid JSON in config file: ${this.configPath}`);
    }

    try {
      this.cachedConfig = this.validator(rawConfig);
      return this.cachedConfig;
    } catch (error) {
      if (error instanceof ConfigValidationError) {
        throw new Error(`Config validation failed: ${error.message}`);
      }
      throw error;
    }
  }

  reload(): T {
    this.cachedConfig = null;
    return this.load();
  }
}

Usage Example

// Define config schema
const configSchema = z.object({
  youtube: z.object({
    enabled: z.boolean(),
    clientId: z.string(),
    clientSecret: z.string(),
  }),
});

type Config = z.infer<typeof configSchema>;

// Create manager
const configManager = new ConfigManager(
  'config.json',
  (config) => configSchema.parse(config)
);

// Load and validate
const config = configManager.load();

// Use config
if (config.youtube.enabled) {
  console.log(`Client ID: ${config.youtube.clientId}`);
}

Best Practices

  1. Validate at startup: Fail fast with clear error messages
  2. Provide defaults: For optional configuration
  3. Type the config: Use TypeScript interfaces or Zod schemas
  4. Document fields: Use JSDoc or Zod descriptions
  5. Environment-specific: Different configs for dev/prod
  6. Secrets separate: Use environment variables for sensitive data
  7. Validation errors: Include field name and expected type