
Product
Introducing Webhook Events for Alert Changes
Add real-time Socket webhook events to your workflows to automatically receive software supply chain alert changes in real time.
@decaf-ts/decoration
Advanced tools
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: 4.1 KB kb gzipped
@decaf-ts/decoration provides two complementary capabilities:
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
Decoration (builder and registry)
Decorator utilities
Metadata store
Constants and types
Design highlights
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.
Enable experimental decorators and decorator metadata in tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Import reflect-metadata once (before decorators execute):
import "reflect-metadata";
The Decoration class exposes a fluent builder that lets you define base decorators, add flavour-specific extras, or override behaviour entirely.
Important behaviour note:
define() registers (or replaces) the base decorators for a key/flavour. If you call define() again for the same key/flavour, the previously registered base decorators are replaced with the new ones.extend() (or providing extras) registers additional flavour-specific decorators that are applied after the base decorators. Crucially, calls to define() will NOT remove or clear previously registered extras — extras persist until they are explicitly changed via extend() (or a define()/apply() that provides explicit extras).These guarantees let you safely replace base behaviour without losing already-registered platform-specific additions.
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; // true
(DefaultComponent as any).__tag; // "base"
If you need to change the base behaviour later, call define() again for the same key/flavour — this REPLACES the previously registered base decorators but does not remove extras that were registered with extend().
const calls: string[] = [];
const baseA = () =>
Decoration.for("widget")
.define(((t: any) => {
calls.push(`baseA:${t.name}`);
}) as any)
.apply();
Decoration.for("widget")
.extend(((t: any) => {
calls.push(`extra:${t.name}`);
}) as any)
.apply();
@baseA()
class Widget1 {}
// Later we replace the base behaviour for the same key. Extras remain.
Decoration.for("widget")
.define(((t: any) => {
calls.push(`baseB:${t.name}`);
}) as any)
.apply();
@Decoration.for("widget").apply()
class Widget2 {}
// calls === [
// `baseA:Widget1`,
// `extra:Widget1`,
// `baseB:Widget2`, // base replaced
// `extra:Widget2` // extra persists
// ]
// Register the same base behaviour as above.
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; // "web"
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; // undefined – overridden
(MobileComponent as any).__mobile; // true
The builder throws when misused; tests assert these guards and you can rely on them in your own code.
const base = Decoration.for("guarded");
// Missing key before define/extend
expect(() => (new Decoration() as any).define(() => () => undefined)).toThrow();
// Multiple overridable decorators are rejected
const overridable = {
decorator: (() => ((target: any) => target)) as any,
args: [],
};
expect(() => base.define(overridable as any, overridable as any)).toThrow();
// Extending the default flavour is blocked
expect(() => Decoration.for("guarded").extend(((t: any) => t) as any)).toThrow();
Helper factories under @decaf-ts/decoration push metadata into the shared store.
import { metadata, Metadata } from "@decaf-ts/decoration";
@metadata("role", "entity")
class User {}
Metadata.get(User, "role"); // "entity"
import { prop, Metadata } from "@decaf-ts/decoration";
class Article {
@prop()
title!: string;
}
Metadata.type(Article, "title") === String; // true
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;
}
import { propMetadata, Metadata } from "@decaf-ts/decoration";
class Product {
@propMetadata("column", "price")
price!: number;
}
Metadata.get(Product, "column"); // "price"
Metadata.type(Product, "price") === Number; // true
import { description, Metadata } from "@decaf-ts/decoration";
@description("User entity")
class User {
@description("Primary email address")
email!: string;
}
Metadata.description(User); // "User entity"
Metadata.description<User>(User, "email" as keyof User); // "Primary email address"
Metadata centralises all recorded information. The snippets below exercise the same flows as metadata.test.ts and the integration suite.
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); // "Person model"
Metadata.properties(Person); // ["name"]
const mirror = Object.getOwnPropertyDescriptor(Person, DecorationKeys.REFLECT);
mirror?.enumerable; // false
(Metadata as any).mirror = false;
Metadata.set(Person, `${DecorationKeys.DESCRIPTION}.class`, "No mirror");
Object.getOwnPropertyDescriptor(Person, DecorationKeys.REFLECT); // undefined
(Metadata as any).mirror = true; // reset when you are done
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); // ["get"]
Metadata.params(Service, "get"); // []
Metadata.return(Service, "get") === String; // true
Metadata.type(Person, "name"); // Reflects design type recorded by @prop()
Metadata.get(Person); // Full metadata payload for advanced inspection
Metadata.get(Person, DecorationKeys.CONSTRUCTOR); // Underlying constructor reference
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
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`)); // "Organization"
console.log(Metadata.type(Org, "name") === String); // true
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)); // ["name", "size"]
Description: Disable mirroring to the constructor if desired.
import { Metadata, DecorationKeys } from "@decaf-ts/decoration";
class Temp {}
;(Metadata as any).mirror = false; // disable
Metadata.set(Temp, `${DecorationKeys.DESCRIPTION}.class`, "Temporary");
console.log(Object.getOwnPropertyDescriptor(Temp, DecorationKeys.REFLECT)); // undefined
// Re-enable when done
;(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); // "decaf"
console.log(ObjectKeySplitter); // "."
console.log(DecorationKeys.PROPERTIES); // "properties"
### Related
[](https://github.com/decaf-ts/ts-workspace)
### Social
[](https://www.linkedin.com/in/decaf-ts/)
#### Languages




## Getting help
If you have bug reports, questions or suggestions please [create a new issue](https://github.com/decaf-ts/ts-workspace/issues/new/choose).
## Contributing
I am grateful for any contributions made to this project. Please read [this](./workdocs/98-Contributing.md) to get started.
## Supporting
The first and easiest way you can support it is by [Contributing](./workdocs/98-Contributing.md). 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](./LICENSE.md).
By developers, for developers...
FAQs
Decoration and metadata mechanisms for decaf-ts
We found that @decaf-ts/decoration demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Product
Add real-time Socket webhook events to your workflows to automatically receive software supply chain alert changes in real time.

Security News
ENISA has become a CVE Program Root, giving the EU a central authority for coordinating vulnerability reporting, disclosure, and cross-border response.

Product
Socket now scans OpenVSX extensions, giving teams early detection of risky behaviors, hidden capabilities, and supply chain threats in developer tools.