frevd
frevd

Reputation: 1937

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.

Upvotes: 72

Views: 42213

Answers (7)

Rok Strniša
Rok Strniša

Reputation: 7202

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.

Upvotes: 0

d2vid
d2vid

Reputation: 2302

@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

Upvotes: 0

Noam Gal
Noam Gal

Reputation: 1292

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

Upvotes: 5

Auston
Auston

Reputation: 315

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

Upvotes: -1

frevd
frevd

Reputation: 1937

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)

Upvotes: -1

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249606

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

Upvotes: 47

Fenton
Fenton

Reputation: 250922

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

Upvotes: 12

Related Questions