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:
- 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.
- Schema Definition: For each entity, define a corresponding schema that specifies column names, types, and constraints (e.g., primary key, not null).
- 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.
- 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.
- 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
createorupdateshould result in a compile-time error. - 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
createwith an instance of the entity should conceptually store it. - Calling
findOnewith a valid ID should return an instance of the entity, correctly typed. - Calling
updatewithPartial<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 createdUserinstance with an assignedid.findOne(1): Would return theUserobject withid: 1.update(1, { isActive: false }): Would return the updatedUserobject.
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
idof typenumber, 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 yourtsconfig.jsonto enable them (experimentalDecorators,emitDecoratorMetadata). - Type Guards and Utility Types: Explore how utility types like
Partialand 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.