TypeScript Schema Validation Types
Data validation is a crucial aspect of building robust applications, ensuring that incoming data conforms to expected structures and types. This challenge focuses on implementing a flexible and type-safe schema validation system in TypeScript, allowing you to define complex data structures and validate against them with confidence.
Problem Description
Your task is to create a TypeScript system for defining and validating data against a predefined schema. This system should be highly type-safe, meaning that the types of your validated data should be inferred directly from the schema definition.
Key Requirements:
- Schema Definition: Design a way to define schemas for various data types, including primitives (string, number, boolean, null, undefined), arrays, and objects.
- Type Inference: The system should be able to infer the TypeScript type of the validated data based on the schema definition.
- Validation Function: Implement a
validatefunction that takes a schema and a value, and returns either the validated (and potentially transformed) value if it conforms to the schema, or throws an error indicating the validation failure. - Support for Nested Structures: The schema should support nested objects and arrays, allowing for complex data structures.
- Custom Error Messages: Provide a mechanism for specifying custom error messages for validation failures.
Expected Behavior:
- If the input value conforms to the schema, the
validatefunction should return the value with its type correctly inferred according to the schema. - If the input value does not conform to the schema, the
validatefunction should throw a descriptiveValidationError.
Edge Cases:
- Handling
nullandundefinedvalues explicitly for fields. - Validating empty arrays and objects.
- Deeply nested structures.
Examples
Example 1: Simple Object Validation
// Schema Definition
const userSchema = {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' },
isActive: { type: 'boolean' },
},
required: ['name', 'age'],
};
// Input Data
const validUserData = {
name: 'Alice',
age: 30,
isActive: true,
};
// Expected Output (or inferred type)
type ValidUser = {
name: string;
age: number;
isActive?: boolean; // isActive is not required
};
// Validation Call (conceptual)
// const validatedUser: ValidUser = validate(userSchema, validUserData);
// This should succeed.
const invalidUserDataMissingName = {
age: 25,
};
// Validation Call (conceptual)
// try {
// validate(userSchema, invalidUserDataMissingName);
// } catch (e) {
// console.log(e.message); // Expected: "name is a required property."
// }
const invalidUserDataWrongType = {
name: 'Bob',
age: 'twenty',
};
// Validation Call (conceptual)
// try {
// validate(userSchema, invalidUserDataWrongType);
// } catch (e) {
// console.log(e.message); // Expected: "age must be a number."
// }
Example 2: Nested Array and Object Validation
// Schema Definition
const productSchema = {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
tags: {
type: 'array',
items: { type: 'string' },
},
dimensions: {
type: 'object',
properties: {
width: { type: 'number' },
height: { type: 'number' },
},
required: ['width'],
},
price: { type: 'number', nullable: true }, // Allowing null price
},
required: ['id', 'name', 'tags', 'dimensions'],
};
// Input Data
const validProductData = {
id: 'prod-123',
name: 'Laptop',
tags: ['electronics', 'computer'],
dimensions: {
width: 35.5,
height: 25.0,
},
price: 1200.50,
};
// Expected Output (or inferred type)
type ValidProduct = {
id: string;
name: string;
tags: string[];
dimensions: {
width: number;
height?: number; // height is not required
};
price: number | null;
};
// Validation Call (conceptual)
// const validatedProduct: ValidProduct = validate(productSchema, validProductData);
// This should succeed.
const invalidProductDataArrayItem = {
id: 'prod-456',
name: 'Mouse',
tags: ['electronics', 123], // Invalid item type in tags
dimensions: {
width: 10,
},
};
// Validation Call (conceptual)
// try {
// validate(productSchema, invalidProductDataArrayItem);
// } catch (e) {
// console.log(e.message); // Expected: "tags[1] must be a string."
// }
const invalidProductDataNestedObject = {
id: 'prod-789',
name: 'Keyboard',
tags: ['accessories'],
dimensions: {
height: 5, // Missing required 'width'
},
};
// Validation Call (conceptual)
// try {
// validate(productSchema, invalidProductDataNestedObject);
// } catch (e) {
// console.log(e.message); // Expected: "dimensions.width is a required property."
// }
Example 3: Handling Nullable and Optional Fields
// Schema Definition
const eventSchema = {
type: 'object',
properties: {
title: { type: 'string' },
description: { type: 'string', nullable: true }, // description can be null
timestamp: { type: 'number' },
attendees: {
type: 'array',
items: { type: 'string' },
optional: true, // attendees array is optional
},
},
required: ['title', 'timestamp'],
};
// Input Data
const eventWithNullDescription = {
title: 'Team Meeting',
description: null,
timestamp: 1678886400000,
};
// Expected Output (or inferred type)
type EventWithNullDesc = {
title: string;
description: string | null;
timestamp: number;
attendees?: string[] | undefined; // attendees is optional
};
// Validation Call (conceptual)
// const validatedEvent: EventWithNullDesc = validate(eventSchema, eventWithNullDescription);
// This should succeed.
const eventWithUndefinedAttendees = {
title: 'Webinar',
timestamp: 1678972800000,
// description is missing, which is allowed because it's nullable.
// attendees is missing, which is allowed because it's optional.
};
// Expected Output (or inferred type)
type EventWithUndefinedAttendees = {
title: string;
description: string | null;
timestamp: number;
attendees?: string[] | undefined;
};
// Validation Call (conceptual)
// const validatedEvent2: EventWithUndefinedAttendees = validate(eventSchema, eventWithUndefinedAttendees);
// This should succeed.
Constraints
- Your schema definition should support at least the following types:
'string','number','boolean','null','array','object'. - The
validatefunction must be able to infer the correct TypeScript type of the validated data. - The
ValidationErrorshould include a clear and informative message indicating the path to the invalid data (e.g.,user.address.street). - The schema definition should support optional properties (fields that may be absent) and nullable properties (fields that can be
null). - Performance is not a primary concern, but your solution should not be excessively inefficient for moderately complex schemas and data.
Notes
- Consider how you will represent the schema itself. An object structure with
type,properties,items,required,optional, andnullablekeys is a good starting point. - Think about how to recursively traverse both the schema and the input data.
- For type inference, you'll likely need to leverage TypeScript's advanced type features like conditional types, mapped types, and infer keywords.
- You can define a base schema type and then extend it for specific types (string, number, object, etc.) to build your schema definition language.
- The
validatefunction's return type should be generic, allowing TypeScript to infer the specific validated type. For example,function validate<T>(schema: Schema, value: unknown): T. - You might want to define a custom
ValidationErrorclass for clearer error handling.