73

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.

4
  • someObject contains more fields and you want extract only the fields in ISpecific ? Commented Jun 13, 2018 at 14:24
  • Yes, I just found a solution, see below. Commented Jun 13, 2018 at 14:31
  • There is no solution yet in TypeScript due to lack of Reflection or language-level support. One has to explicitly specify the members to include (or specify those to ditch in a destruction pattern). Commented Jun 13, 2018 at 15:47
  • This is currently not possible in TypeScript and probably will never be. The design goals says the folowing as a non-goal: 5. Add or rely on run-time type information in programs, or emit different code based on the results of the type system. Instead, encourage programming patterns that do not require run-time metadata. You have to make a solution that works at runtime. At least if play with mapped types for that solution the compiler will still report errors if output is not compatible with your interface. Commented Jun 13, 2018 at 17:22

7 Answers 7

47

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); 
Sign up to request clarification or add additional context in comments.

8 Comments

Beautiful! That looks much better than my factoryfunction+implementor solution I used so far. I must read the Typescript specs, didn't know about the keyof operator (nor the for..of loop).
Interesting way to use type inference by currying with TActual. Thanks also for pointing out the Record type definition (had to read about type definitions as well).
Unfortunately though, since we have to restate all the members again, it is (although it looks more sophisticated) just the same as (equivalent to) merely forming a new partial object using the members of the full object, e.g.: let subset = { someObject.A, someObject.B }; Too bad there is no way to determine members of an interface to extract them without stating them explicitly.
I just had an idea: use a decorator on every member and have that decorator collect the members in a hidden property somewhere, which is the set of properties then extracted from the object using a simple function (no restating of anything). I'll try that..
Thank you for the fine code! @TitianCernicova-Dragomir
|
13

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));

4 Comments

Thanks, but I want to publish the result, so there shall be no access to other members (not only hidden by the compiler). My Interface is an API for consumers, while my Class which implements the interface contains private logic that shall not be accessible. Yes, I could select the members explicitly (or ditch the others as you show), but I was curious whether there is any language feature to do that implicitly. Reflection of interface members is also not possibly using in TypeScript it seems.
PS: The API consumers are regular JavaScript users, hence all TypeScript safety goes out the window and hidden members are accessible if present.
@frevd - the second example satisfies that need, although you might want to follow the convention of having an explicit API response class to ensure no data leaks, and use one of the many auto-mapper clones to map, or by creating an explicit mapping method. One of the big security holes is accidentally exposing data via an API so being bulletproof in that area is a good thing.
The second example works for me, because it was only one property I wanted to remove, maybe a few eventually. But it does introduce a source of future bugs if I forget to add extra excludes if I expand the full interface later.
5

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à! :)

Comments

0

@titian is correct that typescript interfaces/types don't exist at runtime, but the executable code DOES exist at compile-time!

If you don't need a class, here's a much shorter, much less-to-type solution that takes a type or instance and a simple array of strings as keys:

  const pickSafely = <ObjectType>(keys: readonly `${string & keyof ObjectType}`[]) => {
    return (object: any) => {
      const resultObject: ObjectType = {} as unknown as ObjectType;
      for (let index = 0; index < keys.length; index += 1) {
        const key = keys[index] as unknown as keyof ObjectType;
        resultObject[key] = object[key];
      }

      return resultObject as ObjectType;
    }
  }

This approach will save you keystrokes when it's time to use it:

  // Imagine this came from your database.
  const user = {
    firstName: 'Bill',
    favouriteColor: 'green',
    creditCard: 'what is this doing here?',
  };

  // Imagine this is your model.
  type User = {
    firstName?: string;
    favouriteColor?: string;
  }
  const userKeys = ['firstName', 'favouriteColor'] as const;
  const pickUser = pickSafely<User>(userKeys); // No type error.

  // And here's your application usage.
  const safeUser = pickUser(user);

But more importantly, it protects you from picking a key that is NOT allowed by the type. This is useful if you want to use pickSafely to sanitize data coming from a user or strip out fields from a database responses before sending them over the wire.

  // Imagine this came from your database.
  const user = {
    firstName: 'Bill',
    favouriteColor: 'green',
    creditCard: 'what is this doing here?',
  };

  // Imagine this is your model.
  type User = {
    firstName?: string;
    favouriteColor?: string;
  }
  const userKeysWhoopsie = ['firstName', 'favouriteColor', 'creditCard'] as const;
  const pickUserUhOh = pickSafely<User>(userKeysWhoopsie); // Shows a type error - hmm, picking a property you shouldn't?

  // In your application
  const pwndUser = pickUser(user); // This won't execute, it won't compile.

The magic bit of this solution is using Template Literal Types to dynamically generate a union of literal types from a normal Type.

This does NOT protect you from adding a property to ObjectType and forgetting to add it to keys - think of it as a whitelist that is itself whitelisted by the type ;)

Here's the type error this code produces in VSCode: VSCode type error

Comments

0

You can solve this in a clean way using the Zod library:

import { z } from "zod";

export const ISPECIFIC_SCHEMA = z.object({
    A: z.string(),
    B: z.string(),
});
export type ISpecific = z.infer<typeof ISPECIFIC_SCHEMA>;

const someObject = { A: "a", B: "b", C: "c" };
const subset = ISPECIFIC_SCHEMA.parse(someObject);
console.log(subset); // { A: 'a', B: 'b' }

This works, because Zod preserves type information (ISPECIFIC_SCHEMA) at runtime.

With this solution, you only need to specify the type once.

Comments

-1

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)

3 Comments

May I just point out that this is not a good idea, for one you can only extract one interface, which you could live with. But the biggest problem is that there is no relation between the interface and the decorator. You know that you must apply the decorator to interface fields but there is no enforcement of this. If the interface changes you will not know you need to change the class until the bug report comes in. The other solution ensures compile time errors if there is a miss match.
Yeah it's limited, but less painful than adding members. If the interface changes, the compiler forces me to implement the new member (since the class itself implements the interface). It does not force me to apply the decorator to the new method, which I will have to remember to put in front of it (not a biggy). My example is only an example, of course you can make it more sophisticated, supporting multiple sets (e.g. @publish('api1') or even @publish<IApi>()), property accessors etc. For now that's all I need (not a generic solution) and you helped pointing out the way to go, thanks again :)
Maybe ES7 will come with some feature to cut out a portion of the object based on one of its prototypes, TypeScript cannot really enforce that.
-1

unfortunately not, you need especify and repeat the parts.

export interface Machine {
  id: string;
  name: string;
  logo: string;
  location: Location;
  products: Item[];
  restriction: string;
  categories: string[];
  star: number;
  distance: number;
}

export interface MachineIdentification {
  id: string;
  name: string;
  logo: string;
  location: Location;
}

//Object Destructuring fat object in parts
const {id, name, logo, location} = machine; 

//Compose a ligth object from parts and apply interface type
const mi: MachineIdentification = {id, name, logo, location}; 

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.