Compile-Time Shape Validation with Conditional Types
Compile-time validation is a powerful feature of TypeScript that allows you to catch errors before runtime. This challenge focuses on creating a reusable utility type that validates the shape of an object at compile time, ensuring it conforms to a specified structure. This is incredibly useful for ensuring data integrity and preventing unexpected errors when working with complex data structures.
Problem Description
You need to create a TypeScript type called ValidateShape that takes two type parameters: T (the type to validate) and Shape (the expected shape). ValidateShape<T, Shape> should return T if T conforms to the shape Shape, and never otherwise. Conforming to the shape means that T must have all the properties defined in Shape, and the types of those properties must match.
Key Requirements:
- Complete Coverage: The
Shapemust be a complete description of the expected properties. Missing properties inTshould result innever. - Type Matching: The types of the properties in
Tmust match the corresponding types inShape. - Reusability: The solution should be a generic type that can be used with any object type and shape.
- Compile-Time Error: The validation should fail at compile time, providing a clear error message if the shape doesn't match.
Expected Behavior:
If T matches Shape, ValidateShape<T, Shape> should resolve to T. If T does not match Shape, the TypeScript compiler should produce an error indicating the mismatch.
Edge Cases to Consider:
- Optional Properties: The
Shapetype should not need to account for optional properties. The validation should be strict. - Index Signatures: The solution should not need to handle index signatures. Focus on named properties.
- Nested Objects: The validation should work correctly with nested object properties.
Examples
Example 1:
type Shape = {
name: string;
age: number;
};
type User = {
name: string;
age: number;
};
type ValidatedUser = ValidateShape<User, Shape>; // ValidatedUser is User
type InvalidUser = {
name: string;
// age is missing
};
// type ValidatedInvalidUser = ValidateShape<InvalidUser, Shape>; // Compile-time error: Property 'age' is missing in type '{ name: string; }' but required in type 'Shape'.
Example 2:
type Shape = {
address: {
street: string;
city: string;
};
isActive: boolean;
};
type Person = {
address: {
street: string;
city: string;
};
isActive: boolean;
};
type ValidatedPerson = ValidateShape<Person, Shape>; // ValidatedPerson is Person
type IncorrectPerson = {
address: {
street: string;
city: number; // Incorrect type
};
isActive: boolean;
};
// type ValidatedIncorrectPerson = ValidateShape<IncorrectPerson, Shape>; // Compile-time error: Type 'number' is not assignable to type 'string'.
Example 3:
type Shape = {
id: number;
details: {
description: string;
createdAt: Date;
}
};
type Product = {
id: number;
details: {
description: string;
createdAt: Date;
}
};
type ValidatedProduct = ValidateShape<Product, Shape>; // ValidatedProduct is Product
Constraints
- The solution must be a TypeScript type definition.
- The solution must use only standard TypeScript features (no external libraries).
- The solution must be generic and reusable.
- The validation must occur at compile time.
- The solution should be reasonably efficient (avoid unnecessary complexity).
Notes
Consider using conditional types and mapped types to achieve the desired validation. Think about how to iterate over the properties of the Shape and check if they exist in T with the correct types. The key is to leverage TypeScript's type system to enforce the shape at compile time.