Class decorators are an experimental feature of typescript 4.0 and mixins were supported since typescript 2.2 at least. Unfortunately class decorators do have drawbacks with respect to the type recognition because added attributes are not recognized as is. Here is a comparison of class decorators and mixins after an excellent article from Marius Shulz.
We see on two examples how using decorators is more verbose than using mixins.
Timestamps
Timestamping instances at creation time (TS playground).
function Timestamped<
TBase extends { new(...args: any[]): {} }
>(Base: TBase) {
return class extends Base {
timestamp = Date.now()
}
}
// Create a new class by mixing `Timestamped` into an anonymous class
const TimestampedUser = Timestamped(class {
constructor(public name: string) {}
})
// Instantiate the new `TimestampedUser` class
const user = new TimestampedUser("John Doe")
console.log(user.name)
console.log(user.timestamp)
Here is the same functionality with a decorator (TS playground)
interface ITimestamped {
timestamp: Date
}
function Timestamped<
TBase extends { new(...args: any[]): {} }
>(Base: TBase) {
return class extends Base implements ITimestamped {
timestamp = Date.now()
}
}
// Create a new class by decorating class `TimestampedUser`
@Timestamped
class TimestampedUser{
constructor(public name: string) {}
}
// Instantiate the new `TimestampedUser` class
const user = new TimestampedUser("John Doe") as TimestampedUser & ITimestamped
console.log(user.name)
console.log(user.timestamp)
With a Constructor
Mixins version
function Tagged<
TBase extends { new(...args: any[]): {} }
>(Base: TBase) {
return class extends Base {
tag?: string
constructor(...args: any[]) {
super(...args)
try {this.tag = args[1]}
finally {}
}
}
}
// Create a new class by mixing `Tagged` into an anonymous class
const TaggedUser = Tagged(class {
name: string
constructor(...args: any[]) {
try {this.name = args[0]}
finally {}
}
})
const user = new TaggedUser("Jane Doe", "janedoe")
console.log(user.name)
console.log(user.tag)
Nota bene
In the previous code, we have two constructors. TypeScript implicitly forces them to have somehow the same signature. In that example, we also added a new argument to the constructor of the class obtained by mixins.
Class decorator version
interface ITagged {
tag?: string
}
function Tagged<
TBase extends { new(...args: any[]): {} }
>(Base: TBase) {
return class extends Base {
tag?: string
constructor(...args: any[]) {
super(...args)
try {this.tag = args[1]}
finally {}
}
}
}
// Create a new class by mixing `Tagged` into an anonymous class
@Tagged
class TaggedUser {
name: string
constructor(...args: any[]) {
try {this.name = args[0]}
finally {}
}
}
const user = new TaggedUser("Jane Doe", "janedoe") as TaggedUser & ITagged
console.log(user.name)
console.log(user.tag)