Hone logo
Hone
Problems

Type-Safe Configuration Management in TypeScript

Configuration settings are crucial for any application, determining its behavior and external interactions. Manually managing these settings can lead to runtime errors due to type mismatches, typos, or missing values. This challenge asks you to build a robust, type-safe configuration system in TypeScript that ensures your application's configuration is validated at compile time, preventing common errors.

Problem Description

Your goal is to create a TypeScript module that allows defining and accessing application configuration values in a type-safe manner. This system should:

  1. Define Configuration Schema: Allow developers to define the expected structure and types of their configuration settings using TypeScript interfaces.
  2. Load Configuration: Provide a mechanism to load configuration values from an external source (e.g., a JSON file, environment variables, or a JavaScript object).
  3. Type-Safe Access: Ensure that accessing configuration values at runtime is type-safe, meaning TypeScript will enforce the defined types and catch potential errors during compilation.
  4. Validation: Implement basic validation to ensure all required configuration values are present and conform to their expected types. Missing optional values should be handled gracefully.

Key Requirements:

  • A generic function or class to create a configuration instance.
  • The configuration instance should be strongly typed based on a provided schema.
  • Support for basic primitive types (string, number, boolean) and nested objects.
  • A clear way to specify default values for optional configuration properties.
  • The system should throw a clear error if required configuration values are missing during loading.

Expected Behavior:

  • When defining configuration, developers will provide an interface representing the config structure and a source of configuration data.
  • The system will load the data and return a typed configuration object.
  • Accessing a property on the returned object will yield a value of the correct type, as defined by the schema.
  • If a required property is missing in the source data, an error will be thrown.
  • If an optional property is missing, its default value should be used.

Edge Cases:

  • Handling nested configuration objects.
  • Ensuring type compatibility between the schema and the loaded data.
  • Behavior when the configuration source is empty or malformed.

Examples

Example 1: Basic Configuration

// Schema Definition
interface AppConfig {
  appName: string;
  port: number;
  debugMode: boolean;
}

// Configuration Source
const configSource = {
  appName: "MyAwesomeApp",
  port: 8080,
  debugMode: true,
};

// --- Expected Output ---
// Assume a function createConfig<T>(schema: T, source: any): T exists

const config = createConfig<AppConfig>(configSource);

console.log(config.appName); // Output: "MyAwesomeApp" (string)
console.log(config.port);    // Output: 8080 (number)

// If configSource was missing 'appName', an error would be thrown during createConfig.
// If we tried to access config.nonExistentProperty, TypeScript would raise a compile-time error.

Example 2: Nested Configuration and Default Values

// Schema Definition
interface DatabaseConfig {
  host: string;
  port?: number; // Optional, with default
  username: string;
  password?: string; // Optional, no default
}

interface AppConfig {
  appName: string;
  database: DatabaseConfig;
  logLevel: "info" | "warn" | "error";
}

// Configuration Source
const configSource = {
  appName: "DataProcessor",
  database: {
    host: "localhost",
    username: "admin",
    // port is missing, should use default
    // password is missing, will be undefined
  },
  logLevel: "info",
};

// --- Expected Output ---
// Assume a function createConfig<T>(schema: T, source: any, defaults?: Partial<T>): T exists

const defaults = {
    database: {
        port: 5432,
    }
};

const config = createConfig<AppConfig>(configSource, defaults);

console.log(config.database.host); // Output: "localhost" (string)
console.log(config.database.port); // Output: 5432 (number) - from defaults
console.log(config.database.password); // Output: undefined (string | undefined)
console.log(config.logLevel); // Output: "info" ("info" | "warn" | "error")

// If configSource was missing 'appName' or 'database', an error would be thrown.
// If configSource.database was missing 'username', an error would be thrown.

Example 3: Error Handling for Missing Required Values

// Schema Definition
interface ApiSettings {
  apiKey: string;
  baseUrl: string;
}

// Configuration Source (missing apiKey)
const configSource = {
  baseUrl: "https://api.example.com",
};

// --- Expected Output ---
// Assume createConfig function throws an error for missing required properties

try {
  const config = createConfig<ApiSettings>(configSource);
} catch (error: any) {
  console.error("Configuration Error:", error.message);
  // Expected Output: Configuration Error: Missing required configuration property: apiKey
}

Constraints

  • Your solution should be written entirely in TypeScript.
  • The configuration schema can involve nested objects and optional properties.
  • The configuration source can be an arbitrary JavaScript object, but the system should enforce type safety based on the schema.
  • The solution should be efficient and not introduce significant runtime overhead.
  • The system should handle at least primitive types (string, number, boolean) and nested objects.
  • The system should throw specific, informative errors for validation failures.

Notes

  • Consider how you will map the schema definition to runtime validation.
  • Think about how to handle optional properties and their default values elegantly.
  • The createConfig function signature is left open to your design. You might want to pass the schema definition as a type parameter and the actual configuration data as an argument.
  • For validation, you might want to iterate through the properties defined in the schema and check for their presence and type in the loaded configuration data.
  • Libraries like zod or io-ts offer advanced solutions for this, but for this challenge, aim to build a foundational system yourself.
Loading editor...
typescript