Hone logo
Hone
Problems

Type-Safe Object-Relational Mapper (ORM) in TypeScript

The goal is to build a foundational type-safe ORM in TypeScript. This challenge focuses on creating a system that maps TypeScript classes to database tables, ensuring type safety at compile time for data operations. A type-safe ORM significantly reduces runtime errors by leveraging TypeScript's type system to validate data structures and queries.

Problem Description

You need to implement a simplified ORM that allows users to define database entities as TypeScript classes and interact with them through a type-safe interface. The ORM should handle basic CRUD (Create, Read, Update, Delete) operations and ensure that all data interactions are type-checked against the defined entity schemas.

Key Requirements:

  1. Entity Definition: Users should be able to define database entities using TypeScript classes. Each property in the class should correspond to a column in the database table.
  2. Schema Definition: For each entity, define a corresponding schema that specifies column names, types, and constraints (e.g., primary key, not null).
  3. Database Connection (Mocked): For simplicity, you will mock the actual database interactions. Your ORM should expose methods that would interact with a database, but for this challenge, these methods will return mock data or perform in-memory operations.
  4. CRUD Operations: Implement the following type-safe operations:
    • create(entity: T): Inserts a new entity into the "database".
    • findOne(id: number): Retrieves a single entity by its ID.
    • findAll(): Retrieves all entities.
    • update(id: number, data: Partial<T>): Updates an existing entity with partial data.
    • delete(id: number): Deletes an entity by its ID.
  5. Type Safety: All methods should be strongly typed. When creating, querying, or updating, TypeScript should prevent type mismatches between the entity class and the data being operated on. For instance, attempting to assign a string to a number property during create or update should result in a compile-time error.
  6. Relationships (Optional - for advanced users): If time permits, consider how to represent one-to-one or one-to-many relationships between entities.

Expected Behavior:

  • When a user defines an entity class and registers it with the ORM, the ORM should be aware of its schema.
  • Calling create with an instance of the entity should conceptually store it.
  • Calling findOne with a valid ID should return an instance of the entity, correctly typed.
  • Calling update with Partial<T> should only allow updates to properties defined in the entity, and with the correct types.
  • All interactions should be validated at compile time to the greatest extent possible using TypeScript's features.

Edge Cases to Consider:

  • Handling of primary keys: Assume a simple auto-incrementing numeric ID for all entities.
  • Data validation beyond type checking (e.g., nullability, unique constraints) can be simplified for this challenge. The focus is on type safety.

Examples

Example 1: User Entity

Let's define a User entity and demonstrate its usage.

// Entity Definition
class User {
  id: number;
  username: string;
  email: string;
  isActive: boolean;

  constructor(username: string, email: string, isActive: boolean = true) {
    this.username = username;
    this.email = email;
    this.isActive = isActive;
  }
}

// ORM Setup (Conceptual)
// Assume we have an ORM instance configured for User entities
// const userORM = new ORM<User>('users'); // 'users' is the table name

// Usage
// const newUser = new User("alice", "alice@example.com");
// await userORM.create(newUser); // Type-safe: newUser must match User structure

// const fetchedUser = await userORM.findOne(1);
// console.log(fetchedUser.username); // Type-safe: fetchedUser is typed as User

// await userORM.update(1, { isActive: false }); // Type-safe: only partial User properties allowed
// await userORM.update(1, { age: 30 }); // Compile-time error: 'age' is not a property of User

Output (Conceptual - what the methods would return):

  • create(newUser): Would return the newly created User instance with an assigned id.
  • findOne(1): Would return the User object with id: 1.
  • update(1, { isActive: false }): Would return the updated User object.

Example 2: Product Entity and Type Mismatch Error

// Entity Definition
class Product {
  id: number;
  name: string;
  price: number; // Expected to be a number

  constructor(name: string, price: number) {
    this.name = name;
    this.price = price;
  }
}

// ORM Setup (Conceptual)
// const productORM = new ORM<Product>('products');

// Usage
// const invalidProductData = {
//   name: "Laptop",
//   price: "1200" // Problem: price is a string, but Product expects number
// };

// const newProduct = new Product("Laptop", invalidProductData.price); // This line would fail at compile time if we passed invalidProductData directly
// await productORM.create(newProduct);

Explanation:

If Product.price is defined as number, passing a string literal like "1200" to the constructor or an update operation should result in a TypeScript compile-time error, preventing the insertion of invalid data.

Constraints

  • ORM Instantiation: The ORM should be instantiated per entity type.
  • Primary Key: All entities must have a property named id of type number, which will serve as the primary key.
  • Data Storage: For this challenge, use an in-memory array or Map to simulate the database for each entity type.
  • Concurrency: No need to handle concurrent access or transactions.
  • Database Drivers: Do not implement actual database driver integrations (e.g., PostgreSQL, MySQL). Focus on the TypeScript ORM logic.

Notes

  • Generics are Key: Leverage TypeScript generics extensively to achieve type safety.
  • Decorators (Optional but Recommended): Consider using decorators (@Entity, @Column) to provide a more declarative way to define entities and their schemas, though this is not strictly required if you prefer a programmatic approach. If you use decorators, you'll need to configure your tsconfig.json to enable them (experimentalDecorators, emitDecoratorMetadata).
  • Type Guards and Utility Types: Explore how utility types like Partial and potentially custom type guards can enhance the ORM's functionality.
  • Focus on Compile-Time Safety: The primary success metric is the prevention of type errors during compilation, not necessarily complex runtime error handling.
Loading editor...
typescript