Decorators
A toolkit for creating your own TypeScript decorators: class, field, and method.
Introduction
@tekir/decorators provides factory functions for creating TC39-compatible decorators without writing low-level decorator boilerplate. Use it to build project-specific decorators or your own decorator-based packages. In fact, @tekir/db-decorators is built on top of these primitives.
bun add @tekir/decoratorsClass Decorators
setStatic
The simplest class decorator: sets a static property to a given value.
import { setStatic } from '@tekir/decorators'
const Cacheable = setStatic('cacheTtl')
const Connection = setStatic('connection')
@Cacheable(300)
@Connection('redis')
class UserService {
// UserService.cacheTtl === 300
// UserService.connection === 'redis'
}createClassDecorator
For custom logic, pass a handler function that receives the class and your arguments.
import { createClassDecorator } from '@tekir/decorators'
const Entity = createClassDecorator((target, table: string, connection?: string) => {
target.table = table
target.connection = connection ?? 'default'
})
@Entity('users', 'primary')
class User {
// User.table === 'users'
// User.connection === 'primary'
}Field Decorators
pushToArray
Collects decorated field names into a static array. Great for building whitelists (fillable, searchable, indexable).
import { pushToArray } from '@tekir/decorators'
const Searchable = pushToArray('searchableFields')
class Article {
@Searchable() declare title: string
@Searchable() declare content: string
declare authorId: number // not searchable
}
// Article.searchableFields === ['title', 'content']setInMap
Maps each decorated field name to a value in a static object. Perfect for validation rules, cast types, or column config.
import { setInMap } from '@tekir/decorators'
const Validate = setInMap('validationRules')
class RegisterForm {
@Validate('required|email') declare email: string
@Validate('required|min:8') declare password: string
@Validate('required|min:2') declare name: string
}
// RegisterForm.validationRules === {
// email: 'required|email',
// password: 'required|min:8',
// name: 'required|min:2'
// }createFieldDecorator
Full control: your handler receives the constructor, field name, and your arguments.
import { createFieldDecorator } from '@tekir/decorators'
const Transform = createFieldDecorator((ctor, fieldName, fn: (v: any) => any) => {
if (!ctor.transforms) ctor.transforms = {}
ctor.transforms[fieldName] = fn
})
class User {
@Transform((v) => v.toLowerCase())
declare email: string
@Transform((v) => v.trim())
declare name: string
}
// User.transforms === { email: [Function], name: [Function] }Method Decorators
pushMethodToArray
Collects decorated static methods into an array. Use for boot hooks, init sequences, etc.
import { pushMethodToArray } from '@tekir/decorators'
const OnBoot = pushMethodToArray('bootHooks')
class App {
@OnBoot()
static async seedDatabase() {
// ...
}
@OnBoot()
static async warmCache() {
// ...
}
}
// App.bootHooks === [seedDatabase, warmCache]
// Run all: for (const fn of App.bootHooks) await fn()createEventDecorator
Groups decorated methods by event name in a static object, like lifecycle hooks.
import { createEventDecorator } from '@tekir/decorators'
const On = createEventDecorator('listeners')
class UserEvents {
@On('created')
static async sendWelcomeEmail(user: any) {
// ...
}
@On('created')
static async notifyAdmin(user: any) {
// ...
}
@On('deleted')
static async cleanup(user: any) {
// ...
}
}
// UserEvents.listeners === {
// created: [sendWelcomeEmail, notifyAdmin],
// deleted: [cleanup]
// }createMethodDecorator
Full control: your handler receives the constructor, method name, the function, and your arguments.
import { createMethodDecorator } from '@tekir/decorators'
const Throttle = createMethodDecorator((ctor, methodName, fn, ms: number) => {
if (!ctor.throttles) ctor.throttles = {}
ctor.throttles[methodName] = ms
})
class ApiService {
@Throttle(1000)
static async fetchUsers() { ... }
@Throttle(5000)
static async fetchReports() { ... }
}
// ApiService.throttles === { fetchUsers: 1000, fetchReports: 5000 }compose
Combine multiple class decorators into one.
import { createClassDecorator, compose } from '@tekir/decorators'
const Entity = createClassDecorator((t, name: string) => { t.table = name })
const Timestamps = createClassDecorator((t) => { t.timestamps = true })
const SoftDeletes = createClassDecorator((t) => { t.softDeletes = true })
// Apply all three with one decorator
const Model = compose(
Entity('posts'),
Timestamps(),
SoftDeletes()
)
@Model
class Post {
// Post.table === 'posts'
// Post.timestamps === true
// Post.softDeletes === true
}Real-World Examples
Here is a complete example showing how to build a full model decorator system using only @tekir/decorators primitives:
import { pushToArray, setInMap, createEventDecorator, setStatic } from '@tekir/decorators'
// Define your own project-specific decorators
const Fillable = pushToArray('fillable')
const Hidden = pushToArray('hidden')
const Cast = setInMap('casts')
const Hook = createEventDecorator('hooks')
const Table = setStatic('table')
@Table('users')
class User {
@Fillable() declare name: string
@Fillable() declare email: string
@Fillable() declare password: string
@Hidden() declare password: string
@Cast('json') declare metadata: any
@Cast('boolean') declare isActive: boolean
@Hook('beforeSave')
static async hashPassword(user: any) {
if (user.isDirty('password')) {
user.password = await hash.make(user.password)
}
}
@Hook('afterCreate')
static async sendWelcome(user: any) {
await mail.to(user.email).subject('Welcome!').send()
}
}
// This is exactly what @tekir/db-decorators does internally!Every factory function in @tekir/decorators produces standard TC39 decorators. They work with any class, no base class required.