0
# Decorators
1
2
Decorator preservation and inheritance system for mixed classes. The `decorate` function ensures that decorators are properly inherited when classes are mixed together, solving the problem where decorators from mixin classes are lost.
3
4
## Capabilities
5
6
### Decorate Function
7
8
Wraps decorators to ensure they are inherited during the mixing process.
9
10
```typescript { .api }
11
/**
12
* Wraps decorators to ensure they participate in mixin inheritance
13
* Supports class, property, and method decorators
14
* @param decorator - Any decorator (class, property, or method)
15
* @returns Wrapped decorator that will be inherited during mixing
16
*/
17
function decorate<T extends ClassDecorator | PropertyDecorator | MethodDecorator>(
18
decorator: T
19
): T;
20
```
21
22
**Usage Pattern:**
23
24
Replace regular decorators with `decorate(decorator)` in classes that will be mixed:
25
26
```typescript
27
import { Mixin, decorate } from "ts-mixer";
28
29
// Instead of @SomeDecorator()
30
// Use @decorate(SomeDecorator())
31
32
class Validatable {
33
@decorate(IsString()) // instead of @IsString()
34
name: string = "";
35
36
@decorate(IsNumber()) // instead of @IsNumber()
37
age: number = 0;
38
}
39
```
40
41
## Decorator Types
42
43
### Class Decorators
44
45
Class decorators are inherited and applied to the mixed class:
46
47
```typescript
48
import { Mixin, decorate } from "ts-mixer";
49
50
// Custom class decorator
51
function Entity(tableName: string) {
52
return function<T extends { new(...args: any[]): {} }>(constructor: T) {
53
(constructor as any).tableName = tableName;
54
return constructor;
55
};
56
}
57
58
@decorate(Entity("users"))
59
class User {
60
id: number = 0;
61
name: string = "";
62
}
63
64
@decorate(Entity("profiles"))
65
class Profile {
66
bio: string = "";
67
avatar: string = "";
68
}
69
70
class UserProfile extends Mixin(User, Profile) {
71
fullProfile(): string {
72
return `${this.name}: ${this.bio}`;
73
}
74
}
75
76
// Both decorators are applied to the mixed class
77
console.log((UserProfile as any).tableName); // "profiles" (last one wins)
78
```
79
80
### Property Decorators
81
82
Property decorators are inherited for both instance and static properties:
83
84
```typescript
85
import { Mixin, decorate } from "ts-mixer";
86
import { IsEmail, IsString, Length } from "class-validator";
87
88
class PersonalInfo {
89
@decorate(IsString())
90
@decorate(Length(2, 50))
91
firstName: string = "";
92
93
@decorate(IsString())
94
@decorate(Length(2, 50))
95
lastName: string = "";
96
}
97
98
class ContactInfo {
99
@decorate(IsEmail())
100
email: string = "";
101
102
@decorate(IsString())
103
phone: string = "";
104
}
105
106
class Person extends Mixin(PersonalInfo, ContactInfo) {
107
getFullName(): string {
108
return `${this.firstName} ${this.lastName}`;
109
}
110
}
111
112
// All property decorators are inherited
113
const person = new Person();
114
person.firstName = "John";
115
person.lastName = "Doe";
116
person.email = "john.doe@example.com";
117
118
// Validation decorators work on the mixed class
119
import { validate } from "class-validator";
120
121
validate(person).then(errors => {
122
console.log("Validation errors:", errors);
123
});
124
```
125
126
### Method Decorators
127
128
Method decorators are inherited and applied to methods in the mixed class:
129
130
```typescript
131
import { Mixin, decorate } from "ts-mixer";
132
133
// Custom method decorator for logging
134
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
135
const originalMethod = descriptor.value;
136
137
descriptor.value = function(...args: any[]) {
138
console.log(`Calling ${propertyKey} with args:`, args);
139
const result = originalMethod.apply(this, args);
140
console.log(`${propertyKey} returned:`, result);
141
return result;
142
};
143
144
return descriptor;
145
}
146
147
class Calculator {
148
@decorate(Log)
149
add(a: number, b: number): number {
150
return a + b;
151
}
152
}
153
154
class StringUtils {
155
@decorate(Log)
156
reverse(str: string): string {
157
return str.split("").reverse().join("");
158
}
159
}
160
161
class MathStringUtils extends Mixin(Calculator, StringUtils) {
162
@decorate(Log)
163
addAndReverse(a: number, b: number): string {
164
const sum = this.add(a, b);
165
return this.reverse(sum.toString());
166
}
167
}
168
169
const utils = new MathStringUtils();
170
const result = utils.addAndReverse(12, 34); // Logs all method calls
171
// Calling add with args: [12, 34]
172
// add returned: 46
173
// Calling reverse with args: ["46"]
174
// reverse returned: "64"
175
// Calling addAndReverse with args: [12, 34]
176
// addAndReverse returned: "64"
177
```
178
179
## Advanced Usage Examples
180
181
### Class Validator Integration
182
183
Real-world example with `class-validator` decorators:
184
185
```typescript
186
import { Mixin, decorate } from "ts-mixer";
187
import {
188
IsString, IsNumber, IsEmail, IsBoolean, IsOptional,
189
Length, Min, Max, validate
190
} from "class-validator";
191
192
class UserValidation {
193
@decorate(IsString())
194
@decorate(Length(3, 20))
195
username: string = "";
196
197
@decorate(IsEmail())
198
email: string = "";
199
200
@decorate(IsNumber())
201
@decorate(Min(18))
202
@decorate(Max(120))
203
age: number = 0;
204
}
205
206
class ProfileValidation {
207
@decorate(IsString())
208
@decorate(IsOptional())
209
@decorate(Length(0, 500))
210
bio?: string;
211
212
@decorate(IsBoolean())
213
isPublic: boolean = false;
214
215
@decorate(IsString())
216
@decorate(IsOptional())
217
website?: string;
218
}
219
220
class ValidatedUser extends Mixin(UserValidation, ProfileValidation) {
221
constructor(data: Partial<ValidatedUser>) {
222
super();
223
Object.assign(this, data);
224
}
225
226
async isValid(): Promise<boolean> {
227
const errors = await validate(this);
228
return errors.length === 0;
229
}
230
}
231
232
// Usage with validation
233
const user = new ValidatedUser({
234
username: "johndoe",
235
email: "john@example.com",
236
age: 25,
237
bio: "Software developer",
238
isPublic: true
239
});
240
241
user.isValid().then(valid => {
242
console.log("User is valid:", valid); // true
243
});
244
245
const invalidUser = new ValidatedUser({
246
username: "jo", // too short
247
email: "invalid-email",
248
age: 15 // too young
249
});
250
251
invalidUser.isValid().then(valid => {
252
console.log("User is valid:", valid); // false
253
});
254
```
255
256
### Multiple Decorator Types
257
258
Combining different types of decorators in mixed classes:
259
260
```typescript
261
import { Mixin, decorate } from "ts-mixer";
262
263
// Class decorator
264
function Serializable<T extends { new(...args: any[]): {} }>(constructor: T) {
265
return class extends constructor {
266
serialize() {
267
return JSON.stringify(this);
268
}
269
};
270
}
271
272
// Property decorator
273
function Computed(target: any, propertyKey: string) {
274
console.log(`Property ${propertyKey} is computed`);
275
}
276
277
// Method decorator
278
function Cached(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
279
const cache = new Map();
280
const originalMethod = descriptor.value;
281
282
descriptor.value = function(...args: any[]) {
283
const key = JSON.stringify(args);
284
if (cache.has(key)) {
285
return cache.get(key);
286
}
287
288
const result = originalMethod.apply(this, args);
289
cache.set(key, result);
290
return result;
291
};
292
293
return descriptor;
294
}
295
296
@decorate(Serializable)
297
class DataModel {
298
@decorate(Computed)
299
id: number = 0;
300
301
@decorate(Computed)
302
createdAt: Date = new Date();
303
304
@decorate(Cached)
305
expensiveCalculation(input: number): number {
306
console.log("Performing expensive calculation...");
307
return input * input * input;
308
}
309
}
310
311
@decorate(Serializable)
312
class ExtendedModel {
313
@decorate(Computed)
314
updatedAt: Date = new Date();
315
316
@decorate(Cached)
317
anotherExpensiveCalc(a: number, b: number): number {
318
console.log("Another expensive calculation...");
319
return Math.pow(a, b);
320
}
321
}
322
323
class CompleteModel extends Mixin(DataModel, ExtendedModel) {
324
name: string = "Complete Model";
325
326
summary(): string {
327
return `${this.name} created at ${this.createdAt}`;
328
}
329
}
330
331
const model = new CompleteModel();
332
console.log(model.serialize()); // Serializable decorator works
333
console.log(model.expensiveCalculation(5)); // Cached decorator works
334
console.log(model.expensiveCalculation(5)); // Returns cached result
335
```
336
337
## Configuration
338
339
### Decorator Inheritance Strategy
340
341
The decorator inheritance behavior can be configured via `settings.decoratorInheritance`:
342
343
```typescript
344
import { settings } from "ts-mixer";
345
346
// Configure decorator inheritance strategy
347
settings.decoratorInheritance = "deep"; // default - inherit from all ancestors
348
settings.decoratorInheritance = "direct"; // only from direct mixin classes
349
settings.decoratorInheritance = "none"; // disable decorator inheritance
350
```
351
352
**Strategies:**
353
354
- **`"deep"`** (default): Inherits decorators from all classes in the mixin hierarchy
355
- **`"direct"`**: Only inherits decorators from classes directly passed to `Mixin()`
356
- **`"none"`**: Disables decorator inheritance completely
357
358
```typescript
359
import { Mixin, decorate, settings } from "ts-mixer";
360
361
@decorate(SomeDecorator())
362
class A {}
363
364
@decorate(AnotherDecorator())
365
class B extends A {}
366
367
@decorate(ThirdDecorator())
368
class C {}
369
370
settings.decoratorInheritance = "deep";
371
class DeepMixed extends Mixin(B, C) {}
372
// Inherits: SomeDecorator (from A), AnotherDecorator (from B), ThirdDecorator (from C)
373
374
settings.decoratorInheritance = "direct";
375
class DirectMixed extends Mixin(B, C) {}
376
// Inherits: AnotherDecorator (from B), ThirdDecorator (from C)
377
// Does NOT inherit SomeDecorator from A
378
379
settings.decoratorInheritance = "none";
380
class NoDecorators extends Mixin(B, C) {}
381
// Inherits: No decorators
382
```
383
384
## Limitations
385
386
### ES6 Map Requirement
387
388
The decorator system requires ES6 `Map` support. For older environments, you need a polyfill.
389
390
### Decorator Execution Order
391
392
When multiple classes provide decorators for the same target, they are applied in the order the classes are provided to `Mixin()`.
393
394
### Static vs Instance Context
395
396
Property and method decorators are correctly categorized as static or instance-level and applied to the appropriate context in the mixed class.
397
398
### Performance Considerations
399
400
Decorator inheritance adds some overhead during class creation. For performance-critical applications, consider using `"direct"` or `"none"` inheritance strategies.