Search code examples
typescript

TypeScript - extract interface members only - possible?


Is there a way to dynamically extract members from an object belonging to an interface (i.e. not specifying them again explicitly), like this:

let subset = { ...someObject as ISpecific }; 

Currently I get all members that someObject happens to have. So the spread operator does not work here. Are there any ways to do that yet?

Example:

interface ISpecific { A: string; B: string; }
class Extended implements ISpecific { public A: string = '1'; public B: string = '2'; public C: string = '3'; }

let someObject = new Extended(); 
let subset = { ...someObject as ISpecific }; 
console.log(subset);  // -> { A, B, C } but want { A, B }

TypeScript casts are merely hints for the compiler, not real conversions at runtime.


Solution

  • It can be achieved using decorators (see requirements at the end). It can only be used with methods (copying a property get/set accessor yields its momentary return value only, not the accessor function).

    // define a decorator (@publish) for marking members of a class for export: 
    function publish(targetObj: object, memberKey: string, descriptor: PropertyDescriptor) { 
        if (!targetObj['_publishedMembers']) 
            targetObj['_publishedMembers'] = []; 
        targetObj['_publishedMembers'].push(memberKey); 
    }
    
    // this function can return the set of members of an object marked with the @publish decorator: 
    function getPublishedMembers(fromObj: object) {
        const res = {}; 
        const members = fromObj['_publishedMembers'] || []; 
        members.forEach(member => { res[member] = fromObj[member].bind(fromObj); }); 
        return res; 
    }
    
    // this is for making sure all members are implemented (does not make sure about being marked though): 
    interface IPublishedMembers {
        A(): string; 
        B(): number; 
        C(): void; 
    }
    
    // this class implements the interface and has more members (that we do NOT want to expose): 
    class Full implements IPublishedMembers {
        private b: number = 0xb; 
    
        @publish public A(): string { return 'a'; }
        @publish public B(): number { return this.b; }
        @publish public C(): boolean { return true; }
        public D(): boolean { return !this.C(); } 
        public E(): void { } 
    }
    
    const full = new Full(); 
    console.log(full);  // -> all members would be exposed { A(), B(), b, C(), D(), E() }
    
    const published = getPublishedMembers(full) as IPublishedMembers; 
    console.log(published);  // -> only sanctioned members { A(), B(), C() }
    console.log(published.B());  // -> 11 = 0xb (access to field of original object works)
    

    (This requires the compilerOption "experimentalDecorators":true in your tsconfig.json and an ES5 target, more info at http://www.typescriptlang.org/docs/handbook/decorators.html)