@poppinss/traits
v1.0.1
Published
Dead simple traits for ES6 classes
Downloads
478
Readme
Traits
A tiny library to add support for Traits in Javascript and Typescript projects.
This library enables the support for using traits in your Javascript or Typescript codebase.
Table of contents
- Inheritance in Javascript
- Why not mixins?
- Using Traits
- Installation
- Usage
- Constraints
- Extending Typescript types
Inheritance in Javascript
Since Javascript only allows extending a single class at a time, it can become harder to re-use code which relies on many concerns.
The Javascript inheritance model follows a vertical approach, so inheritance leads to classes that stack upon one another as layers of a cake. For example:
class Person {
}
// These are concerns
class Runner {}
class Walker {}
class Walker extends Person {}
class Runner extends Walker {}
class User extends Runner {}
As you can notice, in order to add capabilities of a Runner
and a Walker
to a User
, you have to stack extend calls on top of each other and have a nested __proto__
to look for properties.
Why not mixins?
Mixins follows the same approach of stacking extend calls on top of each other with using a function to do that instead. For example:
function Runner (Base) {
return class Runner extends Base {}
}
function Walker (Base) {
return class Walker extends Base {}
}
This is infact suggested by many in the Typescript community.
Using Traits
Traits follows a simple approach of copying the members of a trait to the destination class, which means after a trait is applied, that class will be garbage collected right away and you are always working with the destination class object.
Also, this library enforces some constraints to keep the code simpler and easy to reason about.
- Only methods are allowed: Traits cannot define it's own state or properties that are not methods. If a trait needs some state to operate, then it can enforce the parent class to define that.
- Getters/setters are allowed: Getters & setters are allowed, since they operate on a pre-existing state.
- Static properties are allowed: We will copy the static properties to the destination class.
- Prototype properties are allowed: We will copy the prototype properties to the destination class.
Installation
Install the package from npm registry as follows:
npm i @poppinss/traits
# yarn
yarn add @poppinss/traits
Usage
Using as a decorator
import { trait } from '@poppinss/traits'
class Runner {
run () {
console.log(this instanceof User)
}
}
class Walker {
walk () {
console.log(this instanceof User)
}
}
class Person {}
@trait(Runner)
@trait(Walker)
export class User extends Person {}
The trait
decorator will copy the run
and walk
methods to the User
class and this
inside those methods will point towards the User
class instance.
Using as a method
If you are not using decorators, then you can make use of the applyTraits
method instead.
import { applyTraits } from '@poppinss/traits'
class User extends Person {}
applyTraits(User, [Runner, Walker])
module.exports = User
Constraints
As mentioned earlier, the traits cannot define state as it will result in an error.
class Runner {
public username = 'virk'
public run () {
return `${this.username} runs`
}
}
When using Runner
as a trait, a runtime exception will be raised that username
is not allowed to be set. Instead, you can enforce the consumer of trait to define the username
. Doing this in Typescript is even simpler with compile time feedback.
export class Runner {
/**
* It is ok to have uninitialized properties for the
* typescript compiler to work. But assigning them
* a value is not allowed
*/
public username: string
public run () {
return `${this.username} runs`
}
}
type Constructor<T> = { new (): T }
export type ConstructorStatic<T = Constructor<{ username: string }>> = T
Usage
import { trait } from '@poppinss/traits'
import { Runner, ConstructorStatic } from './Runner'
@trait<ConstructorStatic>(Runner)
class User {}
Compile time error
Fix it by defining it inside the User
class.
@trait<ConstructorStatic>(Runner)
class User {
public username: string
}
Extending Typescript types
Unfortunately, Typescript doesn't allow decorators to extend the types of the source class and hence you will have to manually extend the types of the traits you are using. For example:
@trait(Runner)
class UserBase {
public username: string
}
const User = UserBase as unknown as (typeof UserBase & typeof Runner) & {
new (): UserBase & Runner,
}
export default User
after this, the run
method will show up on the User
class as well.
Another option is to make use of interface with the same name as the class
interface User {
run (): string
username: string
}
@trait(Runner)
class User {
public username: string
}