Hone logo
Hone
Problems

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:

  1. Schema Definition: Design a way to define schemas for various data types, including primitives (string, number, boolean, null, undefined), arrays, and objects.
  2. Type Inference: The system should be able to infer the TypeScript type of the validated data based on the schema definition.
  3. Validation Function: Implement a validate function 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.
  4. Support for Nested Structures: The schema should support nested objects and arrays, allowing for complex data structures.
  5. 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 validate function should return the value with its type correctly inferred according to the schema.
  • If the input value does not conform to the schema, the validate function should throw a descriptive ValidationError.

Edge Cases:

  • Handling null and undefined values 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 validate function must be able to infer the correct TypeScript type of the validated data.
  • The ValidationError should 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, and nullable keys 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 validate function'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 ValidationError class for clearer error handling.
Loading editor...
typescript