@orchestr-sh/traits
v0.1.0
Published
PHP-style traits for TypeScript with decorators - bring Laravel's trait system to TypeScript
Downloads
93
Maintainers
Readme
@orchestr-sh/traits
PHP-style traits for TypeScript with decorators - bring Laravel's elegant trait system to TypeScript!
Why Traits?
Traits provide a clean way to reuse code across multiple classes without the limitations of single inheritance. They're perfect for:
- ✨ Horizontal code reuse - Share functionality across unrelated classes
- 🎯 Multiple behaviors - Apply several traits to a single class
- 🔧 Composition over inheritance - Flexible and maintainable
- 🚀 Laravel familiarity - Same patterns you know from PHP
Installation
npm install @orchestr-sh/traits reflect-metadataImportant: Add reflect-metadata import at the top of your entry file:
import 'reflect-metadata';Also ensure your tsconfig.json has:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}Quick Start
import 'reflect-metadata';
import { Trait, UsesTraits } from '@orchestr-sh/traits';
// Define a trait
@Trait()
class Timestampable {
created_at?: Date;
updated_at?: Date;
touch(): void {
this.updated_at = new Date();
}
}
// Use the trait
@UsesTraits(Timestampable)
class User {
name: string = '';
// Declare trait methods for TypeScript
touch!: () => void;
created_at?: Date;
updated_at?: Date;
}
// It just works!
const user = new User();
user.touch();
console.log(user.updated_at); // Current dateFeatures
✅ Multiple Traits
Apply as many traits as you need:
@UsesTraits(Timestampable, SoftDeletes, Notifiable, Auditable)
class Article {
// Methods from all traits are available
}✅ Conflict Resolution
Handle method conflicts elegantly:
@UsesTraits(TraitA, TraitB)
@TraitInsteadOf(TraitA, 'greet', TraitB) // Use TraitA's greet
@TraitAlias(TraitB, 'greet', 'greetB') // Alias TraitB's greet
class MyClass {
greet!: () => string; // From TraitA
greetB!: () => string; // From TraitB (aliased)
}✅ Trait Composition
Traits can use other traits:
@Trait()
@UsesTraits(Timestampable)
class SoftDeletes {
touch!: () => void; // From Timestampable
delete(): void {
this.deleted_at = new Date();
this.touch(); // Use method from composed trait
}
}✅ Type Safety
Full TypeScript support with autocomplete:
const user = new User();
user.touch(); // ✓ Type-safe
user.notify(); // ✓ Type-safe✅ Runtime Inspection
Helper functions to inspect traits at runtime:
import { getTraits, hasTrait, getTraitInfo } from '@orchestr-sh/traits';
const traits = getTraits(user);
const hasTimestamp = hasTrait(user, Timestampable);
const info = getTraitInfo(User);API Reference
Decorators
@Trait()
Marks a class as a trait that can be used by other classes.
@Trait()
class MyTrait {
myMethod(): void {
// Implementation
}
}@UsesTraits(...traits)
Applies one or more traits to a class.
@UsesTraits(Trait1, Trait2, Trait3)
class MyClass {
// Trait methods are now available
}@TraitAlias(trait, originalName, alias)
Creates an alias for a trait method to avoid conflicts.
@UsesTraits(TraitA, TraitB)
@TraitAlias(TraitB, 'save', 'saveB')
class MyClass {
save!: () => void; // From TraitA
saveB!: () => void; // From TraitB (aliased)
}@TraitInsteadOf(useTrait, method, ...insteadOf)
Resolves conflicts by specifying which trait's method to use.
@UsesTraits(TraitA, TraitB)
@TraitInsteadOf(TraitA, 'save', TraitB)
class MyClass {
save!: () => void; // Uses TraitA's save, not TraitB's
}Helper Functions
getTraits(target)
Get all traits used by a class or instance.
const traits = getTraits(user);
// Returns: [Timestampable, SoftDeletes, ...]hasTrait(target, trait)
Check if a class or instance uses a specific trait.
if (hasTrait(user, Timestampable)) {
user.touch();
}getTraitMethods(trait)
Get all method names provided by a trait.
const methods = getTraitMethods(Timestampable);
// Returns: ['touch', 'touchCreation']getTraitProperties(trait)
Get all property names defined by a trait.
const properties = getTraitProperties(Timestampable);
// Returns: ['created_at', 'updated_at']detectConflicts(traits)
Find method conflicts between traits.
const conflicts = detectConflicts([TraitA, TraitB, TraitC]);
// Returns: Map { 'save' => [TraitA, TraitB] }getTraitInfo(target)
Get comprehensive information about traits used by a class.
const info = getTraitInfo(User);
// Returns: {
// traits: [Timestampable, SoftDeletes],
// methods: ['touch', 'delete', 'restore'],
// properties: ['created_at', 'updated_at', 'deleted_at'],
// conflicts: Map {}
// }Examples
Laravel-Style Soft Deletes
@Trait()
class SoftDeletes {
deleted_at?: Date;
delete(): void {
this.deleted_at = new Date();
}
restore(): void {
this.deleted_at = undefined;
}
isDeleted(): boolean {
return this.deleted_at !== undefined;
}
}
@UsesTraits(SoftDeletes)
class Post {
title: string = '';
delete!: () => void;
restore!: () => void;
isDeleted!: () => boolean;
deleted_at?: Date;
}
const post = new Post();
post.delete(); // Soft delete
console.log(post.isDeleted()); // true
post.restore(); // RestoreNotifiable Trait
@Trait()
class Notifiable {
notify(message: string): void {
console.log(`📧 ${message}`);
}
notifyViaEmail(email: string, message: string): void {
console.log(`📧 Sending to ${email}: ${message}`);
}
}
@UsesTraits(Notifiable)
class User {
email: string = '';
notify!: (message: string) => void;
notifyViaEmail!: (email: string, message: string) => void;
}
const user = new User();
user.notify('Welcome!');
user.notifyViaEmail(user.email, 'Your account is ready');Multiple Traits with Composition
@Trait()
class Timestampable {
created_at?: Date;
updated_at?: Date;
touch(): void {
this.updated_at = new Date();
}
}
@Trait()
@UsesTraits(Timestampable)
class Auditable {
audit_log: string[] = [];
touch!: () => void;
addAudit(action: string): void {
this.audit_log.push(`${action} at ${new Date()}`);
this.touch();
}
}
@UsesTraits(Auditable)
class Document {
addAudit!: (action: string) => void;
audit_log!: string[];
updated_at?: Date;
}
const doc = new Document();
doc.addAudit('Created');
doc.addAudit('Updated');
console.log(doc.audit_log);
console.log(doc.updated_at);Comparison to PHP
| Feature | PHP Traits | @orchestr-sh/traits |
|---------|-----------|---------------------|
| Multiple traits | ✅ use A, B; | ✅ @UsesTraits(A, B) |
| Conflict resolution | ✅ insteadof | ✅ @TraitInsteadOf |
| Aliasing | ✅ as | ✅ @TraitAlias |
| Composition | ✅ | ✅ |
| Properties | ✅ | ✅ |
| Type safety | ❌ | ✅ TypeScript! |
| Runtime inspection | ⚠️ Limited | ✅ Full API |
Best Practices
1. Always Declare Trait Members
TypeScript needs to know about trait methods and properties:
@UsesTraits(Timestampable)
class User {
// ✅ Good - TypeScript knows about these
touch!: () => void;
created_at?: Date;
// ❌ Bad - TypeScript won't know these exist
}2. Use Traits for Behavior, Not State
Traits work best for shared behavior:
// ✅ Good - Shared behavior
@Trait()
class Timestampable {
touch() { /* ... */ }
}
// ⚠️ Consider carefully - Shared state
@Trait()
class HasSettings {
settings: Map<string, any> = new Map();
}3. Resolve Conflicts Explicitly
Don't rely on default conflict resolution:
// ⚠️ Implicit - Which save() is used?
@UsesTraits(TraitA, TraitB)
class MyClass { }
// ✅ Explicit - Clear which one wins
@UsesTraits(TraitA, TraitB)
@TraitInsteadOf(TraitA, 'save', TraitB)
class MyClass { }4. Use Descriptive Aliases
Make aliases clear and meaningful:
// ✅ Good - Clear what greetB is
@TraitAlias(TraitB, 'greet', 'greetB')
// ❌ Bad - Unclear
@TraitAlias(TraitB, 'greet', 'g2')TypeScript Tips
Declaring Trait Members
Use the ! operator for trait methods and properties:
@UsesTraits(MyTrait)
class MyClass {
traitMethod!: () => void; // Definite assignment
traitProp?: string; // Optional property
}Creating a Base Trait Type
For shared trait interfaces:
interface ITimestampable {
created_at?: Date;
updated_at?: Date;
touch(): void;
}
@Trait()
class Timestampable implements ITimestampable {
created_at?: Date;
updated_at?: Date;
touch(): void {
this.updated_at = new Date();
}
}Common Patterns
Builder Pattern with Traits
@Trait()
class Buildable<T> {
static make<T>(this: new () => T, attributes: Partial<T>): T {
const instance = new this();
Object.assign(instance, attributes);
return instance;
}
}Observable Pattern
@Trait()
class Observable {
private observers: Function[] = [];
observe(callback: Function): void {
this.observers.push(callback);
}
notify(event: string): void {
this.observers.forEach(cb => cb(event));
}
}Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT © Orchestr
Related Projects
- @orchestr-sh/orchestr - Laravel for TypeScript
- reflect-metadata - Metadata reflection API
Made with ❤️ by the Orchestr team
