Skip to content

Instantly share code, notes, and snippets.

@jakoblorz
Created July 20, 2019 11:45
Show Gist options
  • Save jakoblorz/f622155b99094e063adff4c0a6a49a61 to your computer and use it in GitHub Desktop.
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)
/// <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();
});
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