3.0.0 • Published 1 year ago

@lifaon/traits v3.0.0

Weekly downloads
-
License
MIT
Repository
github
Last release
1 year ago

npm (scoped) npm NPM npm type definitions

🏗️ Traits

  • 😢 Ever felt limited by ES classes ?
  • 👿 Sometimes you feel stuck because of the lack of multi-inheritance
  • 😰 Do you feel constrained by the this keyword ?
  • 🔨 Do you want better private/protected properties ?

Well this library is perfect for you 🦸.

A trait is simply a set of named functions (called methods) grouped into an object.

They are mostly used for complex inheritance (like multi-inheritance, polymorphism) and with objects sharing their methods.

It's some kind of framework for objects and methods.

📦 Installation

yarn add @lifaon/traits
# or
npm install @lifaon/traits --save

🛠️ Example

We'll imagine a class Number that mimics the javascript numbers, and provides some operations like addition, etc...

The file structure:

  • number/: contains all the files for such a Number class
    • number.trait-collection.ts : the definition of this Number class
    • traits/: contains all the traits for this class
      • add/: contains the definition of the add trait
        • add.function-definition.ts: contains the definition of the add function
        • add.trait.ts: contains the definition of the add trait
    • implementations/: contains one or more implementation of the number.trait-collection.ts interface
      • create-number.ts: creates a Number instance

Definition of the add function

First we have to define an add function:

// number/traits/add/add.function-definition.ts
export interface IAddFunction<GValue, GReturn> {
  (
    value: GValue,
  ): GReturn;
}

It accepts a value and return another one. This is just an interface (type) for a function.

Definition of the add trait

Then we have to define the add trait:

// number/traits/add/add.trait.ts
export interface IAddTrait<GValue, GReturn> {
  add: IAddFunction<GValue, GReturn>;
}

This is too just an interface: an object must have an add property that implements the IAddFunction.

Collection or traits

Instead of having a class, we'll continue with pure interfaces:

// number/number.trait-collection.ts
export interface INumber extends // 
  IValueOfTrait<number>,
  IAddTrait<INumber, INumber>
  //
{
}

An INumber, is simply an interface which imposes properties (here our traits). We'll call it a trait collection.

In this case, we juste asked for a valueOf and add property, but we may extend it with many more like substract, multiply etc...

Implementation of a trait

The last step is to create an implementation of our INumber:

// number/implementations/create-number.ts

export function createNumber(
  value: number,
): INumber {
  const valueOf = (): number => {
    return value;
  };

  const add = (
    num: INumber,
  ): INumber => {
    return createNumber(value + num.valueOf());
  };

  return {
    valueOf,
    add,
  };
}

So at the end, we don't use classes, but we build directly objects (the instance of a class). For this we use a function which builds the properties (the traits), and create an object implementing them.

Test it

// main.ts

function main(): void {
  const a = createNumber(1);
  const b = createNumber(2);
  const c = a.add(b);
  console.log(c.valueOf()); // 3
}

main();

FAQ

Do I need typescript ?

YES it's mandatory because traits ARE interfaces that we implement. They are not actual code we run, but just type definitions.

Why no classes ?

Because ES classes doesn't support multi-inheritance nor flexibility. For example, you cannot call the constructor of an ES class with a different this, nor use this before the super(...) call, because of natural and perfectly valid constraints on classes, and OOP. However, this may easily limit our creativity especially when we want intentionally to have multi-inheritance, diamond shapes, or custom constructors (ex: extend an Array and having .map returning an instance of our class instead of an Array).

Hopefully, the ES objects don't have these limits: you can assign to the objects all the properties you want. And this is exactly what we'll do to create instances instead of using classes.

This fells so wrong.

First I was sceptical, but in reality this is far more powerful:

  • creating objects from scratch instead of creating an instance of a class, is as fast in both cases, thanks to the insane improvements of the javascript engines.
  • variables are truly private as they belong only to the scope of the function creating the instance of our object.
  • we have the full control of the implementation of our traits: it's possible to pick functions from "child classes", override some, define custom methods, etc... without the constraints of the this nor super keywords.

How can I create properties that are not functions ? ex: { value: number }

This is not possible, let me explain why:

First, we need to differentiate the data from the functions:

  • The data contains all the variables that an instance requires internally. They are similar to a struct in C. In the example of our INumber, this is the incoming value: number. All these data are private, and only limited to the scope of the function creating the instance. This way, there is no leak nor conflict with the properties of some "mother classes".
  • The functions use these data to perform some actions. However, the data are never directly exposed. This is the equivalent of a method on a class.

In consequence, an object contains only properties being functions (we could compare them to methods). So if we want to access a specific value in the data. We need to create a getXXX function:

export interface IGetValueFunction {
  (): number;
}

export interface IGetValueTrait {
  getValue: IGetValueFunction;
}

export interface INumberExtended extends INumber, //
  IGetValueTrait {
}

export function createNumberExtended(
  value: number,
): INumberExtended {
  const numberInstance: INumber = createNumber(value);

  const getValue = numberInstance.valueOf;

  return {
    ...numberInstance,
    getValue,
  };
}

The same is true for setters.

The seconds point, is that, using only functions instead of getter/setters, limits the possibilities of side effects.

For example, a getter may return different values on two consecutive calls, without setting/updating the value between. By design, it means an undefined behaviour. However, using only functions, developer know that something happen (computed property, etc...), and may expect different results.

How can I do single/multi-inheritance ?

Because we just create objects instead of classes this is extremely easy. Check the example above.

What about instanceof ?

Usually, you won't require it because everything is typed: if something implements an interface, we may consider that it is an instance of it.

However, with union types, you may implement some kind of isXXXType(): boolean for example (ex: isNumberType(): boolean).