Compile-Time Data Validation with Custom Types
Compile-time validation in TypeScript allows you to catch errors before your code even runs, leading to more robust and maintainable applications. This challenge asks you to implement a system for validating data structures at compile time, ensuring that only data conforming to specific rules can be used within your application. This is particularly useful for complex data models where runtime validation can be inefficient and error-prone.
Problem Description
You need to create a generic type utility called Validate that takes a type and a validation function as input. The validation function should accept a value of the input type and return a conditional type: true if the value is valid, and never if the value is invalid. The Validate utility should then return the input type only if the validation function returns true for all possible values of that type. If the validation function returns never for any value, the Validate utility should also return never.
Essentially, Validate<T, V> should behave as follows:
- If
V<T>returnstruefor all possible values ofT, thenValidate<T, V> = T. - If
V<T>returnsneverfor any value ofT, thenValidate<T, V> = never.
This allows you to enforce strict data structure rules at compile time, preventing invalid data from ever reaching your runtime code.
Examples
Example 1:
type StringLengthValidator<T extends string> = T extends `${string}` ? (s: T) => s.length > 5 ? true : never : never;
type ValidStrings = Validate<"hello", StringLengthValidator<"hello">>; // "hello"
type InvalidStrings = Validate<"hi", StringLengthValidator<"hi">>; // never
Explanation: StringLengthValidator checks if a string has a length greater than 5. "hello" satisfies this condition, so ValidStrings is "hello". "hi" does not, so InvalidStrings is never.
Example 2:
type NumberRangeValidator<T extends number> = T extends number ? (n: T) => n >= 0 && n <= 100 ? true : never : never;
type ValidNumbers = Validate<50, NumberRangeValidator<50>>; // 50
type InvalidNumbers = Validate<-10, NumberRangeValidator<-10>>; // never
Explanation: NumberRangeValidator checks if a number is within the range of 0 to 100. 50 is valid, so ValidNumbers is 50. -10 is invalid, so InvalidNumbers is never.
Example 3:
type ObjectValidator<T extends { [key: string]: any }> = T extends { [key: string]: any } ? (obj: T) => obj.name === 'valid' && typeof obj.age === 'number' ? true : never : never;
type ValidObject = Validate<{ name: 'valid'; age: 30 }, ObjectValidator<{ name: 'valid'; age: 30 }>>; // { name: 'valid'; age: 30 }
type InvalidObject = Validate<{ name: 'invalid'; age: 30 }, ObjectValidator<{ name: 'invalid'; age: 30 }>>; // never
Explanation: ObjectValidator checks if an object has a 'name' property equal to 'valid' and an 'age' property that is a number. The first object satisfies this, the second does not.
Constraints
- The validation function
Vmust be a function that accepts a value of typeTand returns a conditional type oftrueornever. - The
Validateutility must work correctly for primitive types (string, number, boolean) as well as objects. - The solution must be a valid TypeScript type utility.
- The solution should be as generic as possible to handle various types and validation functions.
Notes
- This problem requires a deep understanding of TypeScript's conditional types and type inference.
- Consider using distributive conditional types to ensure that the validation function is applied to each member of a union type.
- The key is to leverage TypeScript's type system to perform the validation at compile time, rather than at runtime.
- Think about how to handle edge cases and ensure that the utility behaves as expected for all possible input types. The use of
extendsand inference is crucial.