TypeScript Support
Despite the fact that Arquebus ORM is billed as a TypesScript rewrite (which it is), Arquebus ORM is not type safe, although, we do try to provide TypeScript support with a pragmatic approach by balancing usability and type safety. We have chosen to prioritize intuitive and easy-to-use APIs over complete type safety due to the resources at our disposal and our goals. However, we might invest some time in making certain parts of the ORM type safe in the nearest future.
Basic Usage
Here are some basic examples:
import { Model } from '@h3ravel/arquebus';
// Define a basic model
class User extends Model {
// Optional: declare model property types
declare id: number;
declare name: string;
declare email: string;
}
// Using the model
const user = new User();
user.name = 'John';
await user.save();
// Query example
const users = await User.query().where('age', '>', 18).get();
// Relationship example
class Post extends Model {
declare title: string;
declare content: string;
declare user_id: number;
relationUser() {
return this.belongsTo(User);
}
}
Type Safety Notes
While Arquebus ORM provides TypeScript support, we don't aim for complete type safety. This means:
- Some dynamic features may not have complete type inference
- Certain query builder operations might return
any
type - Relationship type inference may be incomplete
For example:
// Dynamic queries may not have accurate type inference
const result = await User.query()
.select(['name', 'email'])
.where('age', '>', 18)
.first();
// Relationship query type inference might be incomplete
const userWithPosts = await User.query().with('posts').first();
Enhancing Type Safety with Generics
To address the above type inference limitations, Arquebus ORM's query methods support generic types, allowing you to:
- Extend model type definitions
- Specify relationship data types
- Add custom field types
For example:
// Basic query with generics
const user = await User.query().first<
User & {
computed_field: string;
}
>();
// Relationship query with generics
const post = await Post.query().with('user').first<
Post & {
user: User;
}
>();
// Custom query result type
interface CustomUserResult extends User {
total_posts: number;
latest_login: Date;
}
const result = await User.query()
.select(['*'])
.selectRaw('COUNT(posts.id) as total_posts')
.first<CustomUserResult>();
// Complex relationship query types
const userWithPosts = await User.query().with('posts').first<
CustomUserResult & {
posts: Post[];
}
>();
Why This Design?
Our design philosophy is:
- Prioritize Developer Experience: We want our API to remain simple and intuitive, rather than being bogged down by complex type definitions
- Practicality First: In certain scenarios, we choose to sacrifice some type safety for more flexible APIs
- Progressive Type Support: You can gradually add more type definitions as needed
Best Practices
Despite our approach, we still recommend:
- Declaring types for main model properties
- Adding type annotations to critical business logic code
- Using type assertions or custom type guards where type safety is needed
// Declare types for important model properties
class Product extends Model {
declare id: number;
declare name: string;
declare price: number;
declare stock: number;
// Use explicit types for custom methods
async updateStock(quantity: number): Promise<void> {
this.stock += quantity;
await this.save();
}
}