@qiwi/mixin
RnD project to compare various mixin approaches in TypeScript.
Getting started
Requirements
- Node.js
^12.20.0 || ^14.13.1 || >=16.0.0
- TypeScript
>= 3.7 | 4.x
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
- Definition.
A mixin is a special kind of multiple inheritance.
- Is it possible to mix classes with automated type inference?
There're several solutions:
- A subclass factory
- Proto merge + constructor invocation + type cast workarounds
- How to combine OOP and functional mixins?
Apply different merge strategies for each target type and rest args converters
- How to check if composition has a given mixin or not?
Ref Cache / WeakMap
- 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
-
Subclass factory
type Constructor<T = {}> = new (...args: any[]) => T function MixFoo<TBase extends Constructor>(Base: TBase) { return class extends Base { foo() { return 'bar' } } }
-
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)); })
-
Object assignment
const foo = {foo: 'foo'} const fooMixin = (target) => Object.assign(target, foo) const bar = fooMixin({bar: 'bar'})
-
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
-
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
- medium.com/javascript-scene/functional-mixins-composing-software-ffb66d5e731c
- medium.com/ackee/typescript-function-composition-and-recurrent-types-a9efbc8e7736
- medium.com/@michaelolof/typescript-mix-yet-another-mixin-library-29c7a349b47d
- dev.to/miracleblue/how-2-typescript-get-the-last-item-type-from-a-tuple-of-types-3fh3
- dev.to/ascorbic/creating-a-typed-compose-function-in-typescript-3-351i
- mariusschulz.com/blog/mixin-classes-in-typescript
- justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/
- github.com/justinfagnani/mixwith.js
- github.com/amercier/es6-mixin
- github.com/Kotarski/ts-functionaltypes
- www.bryntum.com/blog/the-mixin-pattern-in-typescript-all-you-need-to-know/
- stackoverflow.com/questions/533631/what-is-a-mixin-and-why-are-they-useful
- stackoverflow.com/questions/48372465/type-safe-mixin-decorator-in-typescript
- stackoverflow.com/questions/13407036/how-does-interfaces-with-construct-signatures-work