@qiwi/mixin

RnD project to compare various mixin approaches in TypeScript.

Usage no npm install needed!

<script type="module">
  import qiwiMixin from 'https://cdn.skypack.dev/@qiwi/mixin';
</script>

README

@qiwi/mixin

RnD project to compare various mixin approaches in TypeScript.

Build Status Maintainability Test Coverage npm (tag)

Getting started

Requirements

Node.js ^12.20.0 || ^14.13.1 || >=16.0.0

Install

yarn add @qiwi/mixin
npm i @qiwi/mixin

Usage

import {applyMixins} from '@qiwi/mixin'

interface IA {
  a: () => string
}
interface IB {
  b: () => string
}
class A implements IA {
  a() { return 'a' }
}
const b: IB = {
  b() { return 'b' }
}

const c = applyMixins({}, A, b)
c.a() // 'a'
c.b() // 'b'

const D = applyMixins(A, b)
const d = new D()

d.a() // 'a'
d.b() // 'b'

Exports

The library exposes itself as cjs, esm, umd and ts sources. Follow packages.json test:it:* scripts and integration tests examples if you're having troubles with loading.

Advanced examples

import {
  applyMixinsAsProxy,
  applyMixinsAsMerge,
  applyMixinsAsSubclass,
  applyMixinsAsProto,
  applyMixinsAsPipe
} from '@qiwi/mixin'

interface A {
  a(): string
}
interface B extends A {
  b(): string
}
interface C extends B {
  c(): string
}
interface D {
  d(): number
}
const a: A = {
  a() {
    return 'a'
  },
}
const _a: A = {
  a() {
    return '_a'
  },
}
const b = {
  b() {
    return this.a().toUpperCase()
  },
} as B
const c = {
  c() {
    return this.a() + this.b()
  },
} as C

class ACtor implements A {
  a() {
    return 'a'
  }
  static foo() {
    return 'foo'
  }
}
class BCtor extends ACtor implements B {
  b() {
    return this.a().toUpperCase()
  }
  static bar() {
    return 'bar'
  }
}

class DCtor implements D {
  d() {
    return 1
  }
}

class Blank {}

applyMixinsAsProxy

  type ITarget = { foo: string }
  const t: ITarget = {foo: 'bar'}
  const t2 = applyMixinsAsProxy(t, a, b, c, _a)

  t2.c()  // '_a_A'
  t2.a()  // '_a'
  t2.foo  // 'bar'
  // @ts-ignore
  t2.d    // undefined

applyMixinsAsMerge

  type ITarget = { foo: string }
  const t: ITarget = {foo: 'bar'}
  const t2 = applyMixinsAsMerge(t, a, b, c)

  t === t2  // true
  t2.c()    // 'aA'
  t2.a()    // 'a'
  t2.foo    // 'bar'

applyMixinsAsSubclass

  const M = applyMixinsAsSubclass(ACtor, Blank, BCtor, DCtor)
  const m = new M()

  M.foo()   // 'foo'
  M.bar()   // 'bar'

  m instanceof M // true
  m instanceof ACtor // true
  m.a()     // 'a'
  m.b()     // 'A'
  m.d()     // 1

applyMixinsAsProto

  class Target {
    method() {
      return 'value'
    }
  }
  const Derived = applyMixinsAsProto(Target, ACtor, BCtor, DCtor, Blank)
  const m = new Derived()

  Derived === Target // true
  Derived.foo() // 'foo'
  Derived.bar() // 'bar'

  m.a()   // 'a'
  m.b()   // 'A'
  m.d()   // 1

applyMixinsAsFactory

  const n = (n: number) => ({n})
  const m = ({n}: {n: number}) => ({n: 2 * n})
  const k = ({n}: {n: string}) => n.toUpperCase()
  const e = <T extends {}>(e: T): T & {foo: string} => ({...e, foo: 'foo'})
  const i = <T extends {foo: number}>(i: T): T => i

  const nm = applyMixinsAsPipe(n, m)
  const ie = applyMixinsAsPipe(i, e)

  const v1: number = nm(2).n          // 4
  const v2: string = ie({foo: 1}).foo // 'foo'

Implementation notes

Q&A

  1. Definition.

    A mixin is a special kind of multiple inheritance.

  2. Is it possible to mix classes with automated type inference?

    There're several solutions:

    • A subclass factory
    • Proto merge + constructor invocation + type cast workarounds
  3. How to combine OOP and functional mixins?

    Apply different merge strategies for each target type and rest args converters

  4. How to check if composition has a given mixin or not?

    Ref Cache / WeakMap

  5. What's about mixin factories?

    It's called applyMixins

Definition

A mixin is a special kind of multiple inheritance. It's a form of object composition, where component features get mixed into a composite object so that properties of each mixin become properties of the composite object.

In OOP, a mixin is a class that contains methods for use by other classes, and can also be viewed as an interface with implemented methods.

Functional mixins are composable factories which connect together in a pipeline; each function adding some properties or behaviors.

Perhaps these are not perfect definitions, but we'll rely on them.

Mixin cases

  1. Subclass factory

    type Constructor<T = {}> = new (...args: any[]) => T
    
    function MixFoo<TBase extends Constructor>(Base: TBase) {
      return class extends Base {
        foo() { return 'bar' }
      }
    }
    
  2. Prototype injection

    class Derived {}
    class Mixed {
      foo() { return 'bar' }
    }
    
    Object.getOwnPropertyNames(Mixed.prototype).forEach(name => {
        Object.defineProperty(Derived.prototype, name, Object.getOwnPropertyDescriptor(Mixed.prototype, name));
    })
    
  3. Object assignment

    const foo = {foo: 'foo'}
    const fooMixin = (target) => Object.assign(target, foo)
    const bar = fooMixin({bar: 'bar'})
    
  4. Proxy wrapping

    const mixAsProxy = <P extends IAnyMap, M extends IAnyMap>(target: P, mixin: M): P & M => new Proxy(target, {
      get: (obj, prop: string) => {
        return prop in mixin
          // @ts-ignore
          ? mixin[prop]
          // @ts-ignore
          : obj[prop]
      },
    }) as P & M
    
  5. Functional mixin piping

    const foo = <T>(t: T): T & {foo: string} => ({...t, foo: 'foo'})
    const bar = <T>(t: T): T & {bar: number} => ({...t, bar: 1})
    const foobar = pipe(foo, bar) // smth, that composes fn mixins like `(target) => bar(foo(target))`
    const target = {}
    
    const res = foobar(target)
    

Refs

Alternatives

License

MIT