npm package: @orchestr-sh/traits
Description: PHP-style traits for TypeScript with decorators - bring Laravel's trait system to TypeScript
Author: Orchestr
Latest version: 0.1.0

# @orchestr-sh/traits

> PHP-style traits for TypeScript with decorators - bring Laravel's elegant trait system to TypeScript!

[![npm version](https://badge.fury.io/js/%40orchestr-sh%2Ftraits.svg)](https://www.npmjs.com/package/@orchestr-sh/traits)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## 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

```bash
npm install @orchestr-sh/traits reflect-metadata
```

**Important:** Add `reflect-metadata` import at the top of your entry file:

```typescript
import 'reflect-metadata';
```

Also ensure your `tsconfig.json` has:

```json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}
```

## Quick Start

```typescript
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 date
```

## Features

### ✅ Multiple Traits

Apply as many traits as you need:

```typescript
@UsesTraits(Timestampable, SoftDeletes, Notifiable, Auditable)
class Article {
  // Methods from all traits are available
}
```

### ✅ Conflict Resolution

Handle method conflicts elegantly:

```typescript
@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:

```typescript
@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:

```typescript
const user = new User();
user.touch();  // ✓ Type-safe
user.notify(); // ✓ Type-safe
```

### ✅ Runtime Inspection

Helper functions to inspect traits at runtime:

```typescript
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.

```typescript
@Trait()
class MyTrait {
  myMethod(): void {
    // Implementation
  }
}
```

#### `@UsesTraits(...traits)`

Applies one or more traits to a class.

```typescript
@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.

```typescript
@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.

```typescript
@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.

```typescript
const traits = getTraits(user);
// Returns: [Timestampable, SoftDeletes, ...]
```

#### `hasTrait(target, trait)`

Check if a class or instance uses a specific trait.

```typescript
if (hasTrait(user, Timestampable)) {
  user.touch();
}
```

#### `getTraitMethods(trait)`

Get all method names provided by a trait.

```typescript
const methods = getTraitMethods(Timestampable);
// Returns: ['touch', 'touchCreation']
```

#### `getTraitProperties(trait)`

Get all property names defined by a trait.

```typescript
const properties = getTraitProperties(Timestampable);
// Returns: ['created_at', 'updated_at']
```

#### `detectConflicts(traits)`

Find method conflicts between traits.

```typescript
const conflicts = detectConflicts([TraitA, TraitB, TraitC]);
// Returns: Map { 'save' => [TraitA, TraitB] }
```

#### `getTraitInfo(target)`

Get comprehensive information about traits used by a class.

```typescript
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

```typescript
@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();        // Restore
```

### Notifiable Trait

```typescript
@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

```typescript
@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:

```typescript
@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:

```typescript
// ✅ 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:

```typescript
// ⚠️ 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:

```typescript
// ✅ 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:

```typescript
@UsesTraits(MyTrait)
class MyClass {
  traitMethod!: () => void;  // Definite assignment
  traitProp?: string;         // Optional property
}
```

### Creating a Base Trait Type

For shared trait interfaces:

```typescript
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

```typescript
@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

```typescript
@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](https://github.com/orchestr-sh/orchestr) - Laravel for TypeScript
- [reflect-metadata](https://github.com/rbuckton/reflect-metadata) - Metadata reflection API

---

**Made with ❤️ by the Orchestr team**