Created
July 20, 2019 11:45
-
-
Save jakoblorz/f622155b99094e063adff4c0a6a49a61 to your computer and use it in GitHub Desktop.
Provide Mixins for javascript/typescript using an existing reference: Multiple classes reference a single instance (monostate-like)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <reference types="jest" /> | |
import { Implement } from "./Implement"; | |
test("should inherit methods of base class", () => { | |
const fn = jest.fn(); | |
class Base { | |
implementation() { | |
fn(); | |
} | |
} | |
@Implement(Base, new Base()) | |
class Derived implements Base { | |
implementation(): void { | |
throw new Error("Method not implemented."); | |
} | |
} | |
const derived = new Derived(); | |
derived.implementation(); | |
expect(fn).toBeCalled(); | |
}) | |
test("should inherit get and set of base class", () => { | |
const getFn = jest.fn(); | |
const setFn = jest.fn(); | |
class Base { | |
get name() { | |
getFn(); | |
return "" | |
} | |
set name(v: string) { | |
setFn(); | |
} | |
} | |
@Implement(Base, new Base()) | |
class Derived implements Base { | |
_name: string; | |
name: string; | |
} | |
const derived = new Derived(); | |
const _name = derived.name; | |
expect(getFn).toBeCalled(); | |
derived.name = "" | |
expect(setFn).toBeCalled(); | |
}) | |
test("should inherit fields as get and set of base class", () => { | |
class Base { | |
i: number = 1; | |
} | |
const base = new Base(); | |
@Implement(Base, base) | |
class Derived implements Base { | |
i: number; | |
} | |
const derived = new Derived(); | |
expect(derived.i).toBe(base.i); | |
derived.i = 2; | |
expect(base.i).toBe(derived.i); | |
}) | |
test("should keep correct this context on base class", () => { | |
const fn = jest.fn(); | |
class Base { | |
i = 1; | |
implementation() { | |
fn(); | |
// ensure that 'this' still works | |
expect(this).toBeDefined(); | |
expect(this.i).toBe(1); | |
} | |
} | |
@Implement(Base, new Base()) | |
class Derived implements Base { | |
i: number; | |
implementation() { | |
throw new Error("Method not implemented."); | |
} | |
} | |
const derived = new Derived(); | |
derived.implementation(); | |
expect(fn).toBeCalled(); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type Constructor<T> = new(...args: any[]) => T; | |
export function Implement<Child, Mixin extends object>(base: Constructor<Mixin>, instance: Mixin) { | |
return function(constructor: Constructor<Child>) { | |
for (const name of Object.getOwnPropertyNames(base.prototype)) { | |
if (name == "constructor") { | |
continue | |
} | |
let descriptor = Object.getOwnPropertyDescriptor(base.prototype, name) | |
if (!descriptor) { | |
continue | |
} | |
let injection: PropertyDescriptor = descriptor; | |
if (descriptor.value && typeof descriptor.value == "function") { | |
injection = { | |
...injection, | |
value: function(...args: any[]): any { | |
return descriptor.value.call(instance, ...args); | |
} | |
} | |
} else if (descriptor.get && descriptor.set) { | |
injection = { | |
...injection, | |
get: function(): any { | |
return descriptor.get(); | |
}, | |
set: function(v: any) { | |
descriptor.set(v); | |
} | |
} | |
} | |
Object.defineProperty(constructor.prototype, name, injection); | |
} | |
for (const name of Reflect.ownKeys(instance)) { | |
Object.defineProperty(constructor.prototype, name, { | |
get: function(): any { | |
return instance[name]; | |
}, | |
set: function(v: any) { | |
instance[name] = v; | |
} | |
}) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment