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/decorators

Class 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.