
@decaf-ts/decoration
The decoration module provides a small, composable system for building and applying TypeScript decorators with flavour-aware resolution and a centralized runtime Metadata store. It lets you define base decorators, provide framework-specific overrides and extensions ("flavours"), and record/read rich metadata for classes and their members at runtime.






Documentation available here
Minimal size: 2.7 KB kb gzipped
Description
@decaf-ts/decoration provides two complementary capabilities:
- A small, builder-style API (Decoration) to define and apply decorators that can vary by "flavour" (for example, different frameworks or environments) while keeping a stable key-based API.
- A centralized runtime Metadata store (Metadata) for reading and writing structured information about classes and their members, using reflect-metadata for design-time type hints.
This module aims to standardize how decorators are composed, discovered, and executed across contexts, and how metadata is stored and queried during runtime.
Main building blocks
Constants and types
- DefaultFlavour: the default flavour identifier ("decaf").
- ObjectKeySplitter: delimiter for nested metadata keys (".").
- DecorationKeys: well-known metadata keys (REFLECT, PROPERTIES, CLASS, DESCRIPTION, design:type, etc.).
- DefaultMetadata: the default metadata shape used as a base.
- BasicMetadata / Constructor: utility types that describe metadata structure and a class constructor signature.
Design highlights
- Composable and flavour-aware: The Decoration builder allows different environments or frameworks to contribute distinct decorator behavior under the same semantic key. This is useful when building adapters (e.g., Angular vs React components) without changing user code imports.
- Predictable application order: Default decorators are applied first (or a flavour-specific override is used), followed by extras. Extras can come from the default flavour and/or the resolved flavour.
- Introspectable metadata: The Metadata store makes it straightforward to record and query runtime facts about models, properties, and behavior. This is especially helpful for ORMs, serializers, validators, and UI bindings.
How to Use
Practical examples for every exported surface of @decaf-ts/decoration. All snippets are TypeScript and mirror the behaviour covered by the unit and integration tests.
Prerequisites
-
Enable experimental decorators and decorator metadata in tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
-
Import reflect-metadata once (before decorators execute):
import "reflect-metadata";
Decoration Builder
The Decoration class exposes a fluent builder that lets you define base decorators, add flavour-specific extras, or override behaviour entirely.
1. Register base decorators for the default flavour
import { Decoration } from "@decaf-ts/decoration";
const markAsComponent: ClassDecorator = (target) => {
(target as any).__isComponent = true;
};
const tagFactory = (tag: string): ClassDecorator => (target) => {
(target as any).__tag = tag;
};
const component = () =>
Decoration.for("component")
.define({ decorator: tagFactory, args: ["base"] }, markAsComponent)
.apply();
@component()
class DefaultComponent {}
(DefaultComponent as any).__isComponent;
(DefaultComponent as any).__tag;
const baseComponent = () =>
Decoration.for("component")
.define(((target: any) => target) as ClassDecorator)
.apply();
@baseComponent()
class BaseComponent {}
Decoration.setFlavourResolver(() => "web");
const decorate = () =>
Decoration.flavouredAs("web")
.for("component")
.extend({
decorator: (platform: string): ClassDecorator => (target) => {
(target as any).__platform = platform;
},
args: ["web"],
})
.apply();
@decorate()
class WebComponent {}
(WebComponent as any).__platform;
3. Override decorators for an alternate flavour
const base = () =>
Decoration.for("component")
.define(((target: any) => {
(target as any).__base = true;
}) as ClassDecorator)
.apply();
@base()
class BaseBehaviour {}
Decoration.setFlavourResolver(() => "mobile");
const mobileComponent = () =>
Decoration.flavouredAs("mobile")
.for("component")
.define(((target: any) => {
(target as any).__mobile = true;
}) as ClassDecorator)
.apply();
@mobileComponent()
class MobileComponent {}
(MobileComponent as any).__base;
(MobileComponent as any).__mobile;
4. Enforce builder guard rails
The builder throws when misused; tests assert these guards and you can rely on them in your own code.
const base = Decoration.for("guarded");
expect(() => (new Decoration() as any).define(() => () => undefined)).toThrow();
const overridable = {
decorator: (() => ((target: any) => target)) as any,
args: [],
};
expect(() => base.define(overridable as any, overridable as any)).toThrow();
expect(() => Decoration.for("guarded").extend(((t: any) => t) as any)).toThrow();
Decorator Utilities
Helper factories under @decaf-ts/decoration push metadata into the shared store.
metadata(key, value)
import { metadata, Metadata } from "@decaf-ts/decoration";
@metadata("role", "entity")
class User {}
Metadata.get(User, "role");
prop()
import { prop, Metadata } from "@decaf-ts/decoration";
class Article {
@prop()
title!: string;
}
Metadata.type(Article, "title") === String;
apply(...decorators)
import { apply } from "@decaf-ts/decoration";
const logClass: ClassDecorator = (target) => {
console.log("class", (target as any).name);
};
const withLogging = () => apply(logClass);
const logProperty = () => apply((_, key) => console.log("prop", String(key)));
@withLogging()
class Box {
@logProperty()
size!: number;
}
propMetadata(key, value)
import { propMetadata, Metadata } from "@decaf-ts/decoration";
class Product {
@propMetadata("column", "price")
price!: number;
}
Metadata.get(Product, "column");
Metadata.type(Product, "price") === Number;
description(text)
import { description, Metadata } from "@decaf-ts/decoration";
@description("User entity")
class User {
@description("Primary email address")
email!: string;
}
Metadata.description(User);
Metadata.description<User>(User, "email" as keyof User);
Metadata Runtime Helpers
Metadata centralises all recorded information. The snippets below exercise the same flows as metadata.test.ts and the integration suite.
Set and read nested values with constructor mirroring
import { Metadata, DecorationKeys } from "@decaf-ts/decoration";
class Person {
name!: string;
}
Metadata.set(Person, `${DecorationKeys.DESCRIPTION}.class`, "Person model");
Metadata.set(Person, `${DecorationKeys.PROPERTIES}.name`, String);
Metadata.description(Person);
Metadata.properties(Person);
const mirror = Object.getOwnPropertyDescriptor(Person, DecorationKeys.REFLECT);
mirror?.enumerable;
Opt out of mirroring
(Metadata as any).mirror = false;
Metadata.set(Person, `${DecorationKeys.DESCRIPTION}.class`, "No mirror");
Object.getOwnPropertyDescriptor(Person, DecorationKeys.REFLECT);
(Metadata as any).mirror = true;
Work with method metadata
class Service {
get(): string {
return "value";
}
}
Metadata.set(
Service,
`${DecorationKeys.METHODS}.get.${DecorationKeys.DESIGN_PARAMS}`,
[]
);
Metadata.set(
Service,
`${DecorationKeys.METHODS}.get.${DecorationKeys.DESIGN_RETURN}`,
String
);
Metadata.methods(Service);
Metadata.params(Service, "get");
Metadata.return(Service, "get") === String;
Leverage convenience accessors
Metadata.type(Person, "name");
Metadata.get(Person);
Metadata.get(Person, DecorationKeys.CONSTRUCTOR);
Library Registration
Prevent duplicate registration of flavour libraries via Metadata.registerLibrary.
import { Metadata } from "@decaf-ts/decoration";
Metadata.registerLibrary("@decaf-ts/decoration", "0.0.6");
expect(() =>
Metadata.registerLibrary("@decaf-ts/decoration", "0.0.6")
).toThrow(/already/);
You now have end-to-end examples for every public API: builder setup, decorator helpers, metadata management, and library bookkeeping. Mirror the test suite for additional inspiration when adding new patterns.
Metadata class
- Set and get nested values
Description: Use low-level get/set for arbitrary metadata paths.
import { Metadata, DecorationKeys } from "@decaf-ts/decoration";
class Org {}
Metadata.set(Org, `${DecorationKeys.DESCRIPTION}.class`, "Organization");
Metadata.set(Org, `${DecorationKeys.PROPERTIES}.name`, String);
console.log(Metadata.get(Org, `${DecorationKeys.DESCRIPTION}.class`));
console.log(Metadata.type(Org, "name") === String);
Description: Retrieve the keys that have recorded type info.
import { Metadata } from "@decaf-ts/decoration";
class File {
name!: string;
size!: number;
}
Metadata.set(File, "properties.name", String);
Metadata.set(File, "properties.size", Number);
console.log(Metadata.properties(File));
Description: Disable mirroring to the constructor if desired.
import { Metadata, DecorationKeys } from "@decaf-ts/decoration";
class Temp {}
;(Metadata as any).mirror = false;
Metadata.set(Temp, `${DecorationKeys.DESCRIPTION}.class`, "Temporary");
console.log(Object.getOwnPropertyDescriptor(Temp, DecorationKeys.REFLECT));
;(Metadata as any).mirror = true;
Constants and types
Description: Access well-known keys and defaults when interacting with metadata.
import { DefaultFlavour, ObjectKeySplitter, DecorationKeys } from "@decaf-ts/decoration";
console.log(DefaultFlavour);
console.log(ObjectKeySplitter);
console.log(DecorationKeys.PROPERTIES);
Coding Principles
- group similar functionality in folders (analog to namespaces but without any namespace declaration)
- one class per file;
- one interface per file (unless interface is just used as a type);
- group types as other interfaces in a types.ts file per folder;
- group constants or enums in a constants.ts file per folder;
- group decorators in a decorators.ts file per folder;
- always import from the specific file, never from a folder or index file (exceptions for dependencies on other packages);
- prefer the usage of established design patters where applicable:
- Singleton (can be an anti-pattern. use with care);
- factory;
- observer;
- strategy;
- builder;
- etc;
Related

Social

Languages

Getting help
If you have bug reports, questions or suggestions please create a new issue.
Contributing
I am grateful for any contributions made to this project. Please read this to get started.
Supporting
The first and easiest way you can support it is by Contributing. Even just finding a typo in the documentation is important.
Financial support is always welcome and helps keep both me and the project alive and healthy.
So if you can, if this project in any way. either by learning something or simply by helping you save precious time, please consider donating.
License
This project is released under the MIT License.
By developers, for developers...