Building a Type-Driven Development Framework in TypeScript
This challenge is designed to help you explore and implement a type-driven development approach in TypeScript. You will create a simple framework that leverages TypeScript's type system to define and validate data structures, ensuring consistency and reducing runtime errors. This approach promotes writing more robust and maintainable code by shifting validation from runtime to compile time.
Problem Description
Your task is to build a minimal type-driven development framework in TypeScript. This framework should allow users to define data schemas using TypeScript's type system and then provide a mechanism to validate data against these defined schemas.
Key Requirements:
- Schema Definition: Users should be able to define data schemas using TypeScript interfaces or types.
- Validation Function: Implement a generic
validatefunction that takes a data object and a schema definition as input. - Type Safety: The
validatefunction should leverage TypeScript's type inference to ensure that the input data adheres to the specified schema. - Error Reporting: The validation function should return information about any validation errors encountered. This could be an array of error messages or a more structured error object.
- Basic Data Types: The framework should support validation for primitive types (string, number, boolean) and nested object structures.
Expected Behavior:
- When data perfectly matches the schema, the
validatefunction should indicate success (e.g., returntrueor an empty error list). - When data deviates from the schema (e.g., missing properties, incorrect types), the
validatefunction should clearly report these discrepancies.
Edge Cases to Consider:
- Optional properties in the schema.
- Arrays of specific types.
- Empty objects or null/undefined values where not expected.
Examples
Example 1:
interface User {
id: number;
name: string;
isActive?: boolean; // Optional property
}
const validUserData = {
id: 123,
name: "Alice",
isActive: true,
};
const invalidUserDataMissingName = {
id: 456,
isActive: false,
};
const invalidUserDataWrongType = {
id: "abc",
name: "Bob",
};
// Assuming a `validate` function is implemented:
// validate(validUserData, User) => { isValid: true, errors: [] }
// validate(invalidUserDataMissingName, User) => { isValid: false, errors: ["'name' is a required property."] }
// validate(invalidUserDataWrongType, User) => { isValid: false, errors: ["'id' must be of type number."] }
Explanation: This example demonstrates validating a User object. The first case is valid. The second case fails because the required name property is missing. The third case fails because id is a string instead of a number.
Example 2:
interface Product {
name: string;
price: number;
tags: string[]; // Array of strings
}
const validProductData = {
name: "Laptop",
price: 999.99,
tags: ["electronics", "computer"],
};
const invalidProductDataWrongTagType = {
name: "Mouse",
price: 25.50,
tags: ["accessory", 123], // Tag is a number, not a string
};
// Assuming a `validate` function is implemented:
// validate(validProductData, Product) => { isValid: true, errors: [] }
// validate(invalidProductDataWrongTagType, Product) => { isValid: false, errors: ["Element at index 1 in 'tags' must be of type string."] }
Explanation: This example showcases validation of an array of a specific type. The first Product object is valid. The second fails because one of the elements in the tags array is a number, not a string.
Example 3:
interface Settings {
theme: "light" | "dark"; // Literal types
fontSize: number;
}
const validSettings = {
theme: "dark",
fontSize: 16,
};
const invalidSettingsTheme = {
theme: "blue", // Not a valid theme literal
fontSize: 14,
};
// Assuming a `validate` function is implemented:
// validate(validSettings, Settings) => { isValid: true, errors: [] }
// validate(invalidSettingsTheme, Settings) => { isValid: false, errors: ["'theme' must be one of 'light', 'dark'."] }
Explanation: This example illustrates validating against literal types. The first Settings object is valid. The second fails because the theme value "blue" is not one of the allowed literal values ("light" or "dark").
Constraints
- The framework should be implemented entirely in TypeScript.
- The
validatefunction should be generic, accepting any schema type. - Validation should be performed recursively for nested objects.
- The framework should handle the basic types:
string,number,boolean,null,undefined. - Support for simple arrays (e.g.,
string[],number[]) is required. - Support for optional properties (
?) is required. - Consider performance: While not a strict benchmark, avoid overly complex or inefficient recursive loops.
Notes
- Think about how you can use TypeScript's
keyofandtypeofoperators to introspect the schema. - Consider how to map TypeScript types to runtime validation rules.
- You'll likely need to create a utility type or function to represent the outcome of the validation (e.g., an object containing
isValidand anerrorsarray). - For more complex scenarios like unions, intersections, or mapped types, you might need to make simplifying assumptions or focus on the core requirements for this challenge.
- The goal is to build a framework, so aim for a clean and extensible design.