Declare mapped fields with declare so TypeScript emits no runtime class field that could
shadow Leoric’s attribute accessors. This is the preferred path and does not require a class
decorator.
import { Bone, Column } from 'leoric';
class User extends Bone {
@Column()
declare name: string;
}
Models supplied to connect() or a realm are registered directly and retain their original
constructor, inheritance, and instanceof behavior. Leoric reports an ES class field that
shadows a mapped accessor when it encounters the first ORM-created instance. A lint rule can
report the same mistake before runtime.
Use @Model() only when regular ES2022 fields must be supported:
import { Bone, Column, Model } from 'leoric';
@Model()
class User extends Bone {
@Column()
name!: string;
}
@Model() compiles the definition into a fresh Bone subclass once. It copies supported
methods, accessors, static configuration, and model metadata, but does not execute the
definition’s constructor or instance field initializers. Put mapped defaults in @Column()
metadata. Custom constructors, private instance fields, and ordinary instance initializers
are not supported on compiled definitions.
import { Bone, Column, DataTypes: { SMALLINT } } from 'leoric';
class User extends Bone {
@Column({ primaryKey: true })
declare id: bigint;
@Column({ allowNull: false })
declare name: string;
@Column()
declare createdAt: Date;
@Column()
declare updatedAt: Date;
@Column({ type: SMALLINT })
declare age: number;
}
Here is the list of options supported by @Column() that can be used to customize column definitions:
| option | description |
|---|---|
| primaryKey = false | declare class field as the primary key |
| autoIncrement = false | enable auto increment on corresponding class field, must be numeric type |
| allowNull = true | class field can not be null when persisting to database |
| type = typeof field | override the data type deduced from class field type |
| name = string | actual name of the table field in database |
If type option is omitted, @Column() will try to deduce the corresponding one as below:
| ts type | data type |
|---|---|
| number | INTEGER |
| string | STRING / VARCHAR(255) |
| Date | DATE |
| bigint | BIGINT |
| boolean | BOOLEAN / TINYINT(1) |
Here is an example that is a little bit more comprehensive:
class User extends Bone {
@Column({ name: 'ssn', primaryKey: true, type: VARCHAR(16) })
declare ssn: string;
@Column({ name: 'gmt_create', allowNull: false })
declare createdAt: Date;
}
import User from './user';
class Post extends Bone {
@BelongsTo()
declare user: User;
}
const post = await Post.include('user').first;
assert.ok(post.user.id);
If the foreign key didn’t follow the naming convention, please provide it with:
class Post extends Bone {
@BelongsTo({ foreignKey: 'authorId' })
declare user: User;
}
import Post from './post';
class User extends Bone {
@HasMany()
declare posts: Post[];
}
If the foreign key didn’t follow the naming convention, please provide it with:
class User extends Bone {
@HasMany({ foreignKey: 'authorId' })
declare posts: Post[];
}
In a hasMany association, e.g. one-to-many, the foreign key should be at the associated table. Please refer to our documentation about Associations for more detail.
import Profile from './profile';
class User extends Bone {
@HasOne()
declare profile: Profile;
}
If the foreign key didn’t follow the naming convention, please provide it with:
import Profile from './profile';
class User extends Bone {
@HasOne({ foreignKey: 'ownerId' })
declare profile: Profile;
}
hasOne works almost the same as hasMany, which needs the foreign key to be at the associated table as well.
Whilst both hasOne and belongsTo can be used to create a one-to-one association, the major difference between them is where the foreign key is expected at. If you weren’t familiar with the difference yet, please refer to our documentation about Associations for more detail.
For many-to-many associations, use the through option:
import Tag from './tag';
import TagMap from './tag_map';
class Post extends Bone {
@HasMany({ through: 'tagMaps' })
declare tags: Tag[];
@HasMany()
declare tagMaps: TagMap[];
}
You can add a validate option to @Column() to enable field validation:
class User extends Bone {
@Column({
allowNull: false,
validate: {
isEmail: true,
},
})
declare email: string;
@Column({
validate: {
isUrl: true,
},
})
declare website: string;
}
Here is a comprehensive example showing a full model definition in TypeScript:
import { Bone, Column, BelongsTo, HasMany, DataTypes } from 'leoric';
const { TEXT, JSONB } = DataTypes;
import User from './user';
import Comment from './comment';
export default class Post extends Bone {
@Column({ primaryKey: true, autoIncrement: true })
declare id: bigint;
@Column({ allowNull: false })
declare title: string;
@Column(TEXT)
declare content: string;
@Column(JSONB)
declare extra: Record<string, unknown>;
@Column()
declare userId: bigint;
@Column()
declare createdAt: Date;
@Column()
declare updatedAt: Date;
@Column()
declare deletedAt: Date;
@BelongsTo()
declare user: User;
@HasMany()
declare comments: Comment[];
}
Leoric provides backward-compatible type declarations for TypeScript 4.9 and earlier through typesVersions in package.json. This is handled automatically - no additional configuration is needed.
If you’re using TypeScript <= 4.9, the type declarations from the types/ts4.9/ directory will be used instead of the default ones.
TypeScript integration enables type-safe queries:
// Return type is inferred as Post | null
const post = await Post.findOne({ title: 'Hello' });
// Return type is inferred as Post[]
const posts = await Post.find({ userId: 1 });
// Attributes are type-checked
await Post.create({
title: 'New Post', // OK
content: 'Hello', // OK
// unknown: 'value', // TypeScript error: unknown property
});
To use decorators in TypeScript, ensure the following compiler options are enabled in your tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
The emitDecoratorMetadata option is required for the automatic type inference in @Column() to work. You also need to install reflect-metadata (which is a dependency of Leoric).