Hone logo
Hone
Problems

Static Analysis with Type Guards and Conditional Types: Validating Configuration Objects

Build-time type safety is a cornerstone of TypeScript's power. This challenge focuses on leveraging TypeScript's advanced type system – specifically type guards and conditional types – to perform static analysis of configuration objects during compilation, rather than at runtime. This allows you to catch errors early, improving code reliability and developer experience.

Problem Description

You are tasked with creating a utility function validateConfiguration that statically validates a configuration object against a predefined schema. The schema defines the expected properties, their types, and potentially, whether they are required. The function should return a narrowed type representing the validated configuration, or throw a compile-time error if the configuration is invalid.

The schema will be defined as a type with the following structure:

type ConfigurationSchema<T> = {
  [K in keyof T]?: {
    required?: boolean;
    type: string; // "string", "number", "boolean", "array", "object"
    // Additional properties for specific types (e.g., array elements type, object properties type)
    [key: string]: any;
  };
};

The validateConfiguration function should accept two arguments:

  1. config: The configuration object to validate. Its type will be generic, T.
  2. schema: The configuration schema, also of generic type T.

The function should perform the following checks:

  • Required Properties: For each property marked as required: true in the schema, ensure that the property exists in the configuration object.
  • Type Validation: For each property, verify that its type matches the type specified in the schema. You'll need to use type guards to perform this validation. Consider these type guard implementations:
    • isString(value: any): value is string
    • isNumber(value: any): value is number
    • isBoolean(value: any): value is boolean
    • isArray(value: any): value is any[]
    • isObject(value: any): value is object
  • Nested Object Validation: If a property's type is "object", recursively validate its properties against a nested schema (which will also be provided in the schema).

If any validation fails, the function should throw a compile-time error (using assert or a similar mechanism) with a descriptive message. If the configuration is valid, the function should return the configuration object, narrowed to the type defined by the schema.

Examples

Example 1:

type MyConfig = {
  name: string;
  port: number;
  enabled: boolean;
};

const schema: ConfigurationSchema<MyConfig> = {
  name: { required: true, type: "string" },
  port: { required: true, type: "number" },
  enabled: { required: false, type: "boolean" },
};

const validConfig = { name: "My App", port: 8080, enabled: true };
// validateConfiguration(validConfig, schema) should return MyConfig & { name: string, port: number, enabled: boolean }

const invalidConfig = { name: "My App", enabled: true }; // Missing port
// validateConfiguration(invalidConfig, schema) should throw a compile-time error

Example 2:

type NestedConfig = {
  setting1: string;
  setting2: number;
};

type TopLevelConfig = {
  nested: NestedConfig;
  apiKey: string;
};

const nestedSchema: ConfigurationSchema<NestedConfig> = {
  setting1: { required: true, type: "string" },
  setting2: { required: true, type: "number" },
};

const topLevelSchema: ConfigurationSchema<TopLevelConfig> = {
  nested: { required: true, type: "object", schema: nestedSchema },
  apiKey: { required: true, type: "string" },
};

const validTopLevelConfig = { nested: { setting1: "value", setting2: 123 }, apiKey: "your-api-key" };
// validateConfiguration(validTopLevelConfig, topLevelSchema) should return TopLevelConfig & { nested: NestedConfig, apiKey: string }

const invalidTopLevelConfig = { nested: { setting1: "value" }, apiKey: "your-api-key" }; // Missing setting2
// validateConfiguration(invalidTopLevelConfig, topLevelSchema) should throw a compile-time error

Constraints

  • The type string in the schema can only be "string", "number", "boolean", "array", or "object".
  • For "array" type, the schema should include an elementType property specifying the type of elements in the array.
  • For "object" type, the schema should include a schema property which is a ConfigurationSchema for the nested object.
  • The function must perform static analysis; runtime checks are not acceptable.
  • The function should be generic, accepting configuration and schema types.
  • Error messages should be descriptive enough to pinpoint the validation failure.

Notes

  • Consider using conditional types to narrow the type of the configuration object based on the schema.
  • Type guards are essential for performing type-specific checks.
  • Recursion is required for validating nested objects.
  • The assert keyword (or similar) is the appropriate way to enforce compile-time errors.
  • Think carefully about how to represent the narrowed type after successful validation. You might need to use intersection types.
  • This is a complex challenge that requires a good understanding of TypeScript's advanced type system. Start with simpler cases and gradually build up to more complex scenarios.
  • The provided isString, isNumber, isBoolean, isArray, and isObject type guards are assumed to be available. You don't need to implement them.
Loading editor...
typescript