Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript - extract interface members only - possible?

Tags:

typescript

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.

like image 367
frevd Avatar asked Jun 13 '18 14:06

frevd


3 Answers

Since typescript interfaces don't exist at runtime, we can't use them to guide any runtime behavior, just compile-time type checking. We can however create an object that has the same properties as the interface (with all the properties of type true for example to simplify initialization) and make the compiler trigger an error if this object has any more or less fields then the interface. We can use this object as the guide to what properties we extract:

function extract<T>(properties: Record<keyof T, true>){
    return function<TActual extends T>(value: TActual){
        let result = {} as T;
        for (const property of Object.keys(properties) as Array<keyof T>) {
            result[property] = value[property];
        }
        return result;
    }
}

interface ISpecific { A: string; B: string; }
const extractISpecific = extract<ISpecific>({ 
    // This object literal is guaranteed by the compiler to have no more and no less properties then ISpecific
    A: true,
    B: true
})
class Extended implements ISpecific { public A: string = '1'; public B: string = '2'; public C: string = '3'; }

let someObject = new Extended(); 
let subset = extractISpecific(someObject); 
like image 117
Titian Cernicova-Dragomir Avatar answered Oct 11 '22 03:10

Titian Cernicova-Dragomir


If you want to limit the types you use you can do it simply and safely with:

let subset = someObject as ISpecific; 

The properties will still exist on subset but the compiler will prevent you depending on them, i.e. subset.age will fail below, although the property does still exist.

interface ISpecific {
    name: string;
}

const someObject = {
    name: 'Fenton',
    age: 21
};

let subset = someObject as ISpecific; 

console.log(subset.age);

You could really ditch the properties by destructuring like this, the danger being that you need to include "all the things I don't want" in the list before ...subset.

interface ISpecific {
    name: string;
}

const someObject = {
    name: 'Fenton',
    age: 21
};

let { age, ...subset } = someObject;   

console.log(JSON.stringify(someObject));
console.log(JSON.stringify(subset));
like image 38
Fenton Avatar answered Oct 11 '22 02:10

Fenton


Another easy option I came across is using lodash's pick function. It's a bit tedious, but does the job pretty well.

First, define a class that represents your interface. You will need it later to easily create an object of that class.

class Specific {
  constructor(readonly a?: string, readonly b?: string) {}
}
interface ISpecific extends Specific {}
interface IExtended extends ISpecific {
  c: string;
}

Then let's say that this is the original object you want to extract data from:

const extended: IExtended = { a: 'type', b: 'script', c: 'is cool' };

Now comes the fun part. Get a list of Specific keys based on a new instantiation of the class, and pick those members from the original object.
In other words:

const specificMembers: string[] = Object.keys(new Specific());
const specific: ISpecific = lodash.pick(extended, specificMembers);
console.log(specific); // {a: "type", b: "script"}

Voilà! :)

like image 3
Noam Gal Avatar answered Oct 11 '22 04:10

Noam Gal