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:
config: The configuration object to validate. Its type will be generic,T.schema: The configuration schema, also of generic typeT.
The function should perform the following checks:
- Required Properties: For each property marked as
required: truein the schema, ensure that the property exists in the configuration object. - Type Validation: For each property, verify that its type matches the
typespecified in the schema. You'll need to use type guards to perform this validation. Consider these type guard implementations:isString(value: any): value is stringisNumber(value: any): value is numberisBoolean(value: any): value is booleanisArray(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
typestring in the schema can only be "string", "number", "boolean", "array", or "object". - For "array" type, the schema should include an
elementTypeproperty specifying the type of elements in the array. - For "object" type, the schema should include a
schemaproperty which is aConfigurationSchemafor 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
assertkeyword (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, andisObjecttype guards are assumed to be available. You don't need to implement them.