Skip to content

Instantly share code, notes, and snippets.

@fearthecowboy
Last active June 13, 2018 18:51
Show Gist options
  • Save fearthecowboy/443b0e1a36b60f03bdf1e904d87c541e to your computer and use it in GitHub Desktop.
Save fearthecowboy/443b0e1a36b60f03bdf1e904d87c541e to your computer and use it in GitHub Desktop.
Intersection Proxy for Typescript
/**
* Creates an intersection object from two source objects.
*
* Typescript nicely supports defining intersection types (ie, Foo & Bar )
* But if you have two seperate *instances*, and you want to use them as the implementation
* of that intersection, the language doesn't solve that for you.
*
* This function creates a strongly typed proxy type around the two objects,
* and returns members for the intersection of them.
*
* This works well for properties and member functions the same.
*
* Members in the primary object will take precedence over members in the secondary object if names conflict.
*
* This can also be used to "add" arbitrary members to an existing type (without mutating the original object)
*
* @example
* const combined = intersect( new Foo(), { test: () => { console.log('testing'); } });
* combined.test(); // writes out 'testing' to console
*
* @param primary primary object - members from this will have precedence.
* @param secondary secondary object - members from this will be used if primary does not have a member
*/
export function intersect<T extends object, T2 extends object>(primary: T, secondary: T2): T & T2 {
return <T & T2><any>new Proxy({ primary, secondary }, {
// member get proxy handler
get(target, property, receiver) {
// check for properties on the objects first
const propertyName = property.toString();
if (Object.getOwnPropertyNames(target.primary).indexOf(propertyName) > -1) {
return (<any>target.primary)[property];
}
if (Object.getOwnPropertyNames(target.secondary).indexOf(propertyName) > -1) {
return (<any>target.secondary)[property];
}
// try binding member function
if (typeof ((<any>target.primary)[property]) === 'function') {
return (<any>target.primary)[property].bind(primary);
}
if (typeof ((<any>target.secondary)[property]) === 'function') {
return (<any>target.secondary)[property].bind(secondary);
}
// fallback for inherited?
return (<any>target.primary)[property] || (<any>target.secondary)[property];
},
// member set proxy handler
set(target, property, value) {
const propertyName = property.toString();
if (Object.getOwnPropertyNames(target.primary).indexOf(propertyName) > -1) {
return (<any>target.primary)[property] = value;
}
if (Object.getOwnPropertyNames(target.secondary).indexOf(propertyName) > -1) {
return (<any>target.secondary)[property] = value;
}
return undefined;
}
});
}
@fearthecowboy
Copy link
Author

examples:

export class foo {
  x: number = 10;

  boom() {
    console.log(`BOOM ${this.x}`);
  }
}

export class bar {
  y: number = 100;
  boomTwo() {
    console.log("More Boom");
  }
}

const item = intersect(new foo(), new bar());

console.log(item.x);    // 10
console.log(item.y);    // 100
item.boomTwo();        // More Boom

const item2 = intersect(new foo(), {
  happy: "hello",
  sad: "goodbye",
  kaboom: () => {
    console.log("DOUBLE BOOM");
  }
})

console.log(item2.sad);    // goodbye

item2.sad = "sadder";

console.log(item2.sad);   // sadder


item2.boom();    // BOOM 10

item2.kaboom();  // DOUBLE BOOM

@fearthecowboy
Copy link
Author

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment