'How to reduce javascript object to only contain properties from interface

When using typescript a declared interface could look like this:

interface MyInterface {
  test: string;
}

And an implementation with extra property could be like this:

class MyTest implements MyInterface {
  test: string;
  newTest: string;
}

Example (here the variable 'reduced' still contain the property 'newTest'):

var test: MyTest = {test: "hello", newTest: "world"}

var reduced: MyInterface = test; // something clever is needed

Question

In a general way, how can you make the 'reduced' variable to only contain the properties declared in the 'MyInterface' interface.

Why

The problem occur when trying to use the 'reduced' variable with angular.toJson before sending it to a rest service - the toJson method transforms the newTest variable, even if it's not accessible on the instance during compile, and this makes the rest service not accept the json since it has properties that shouldn't be there.



Solution 1:[1]

TS 2.1 has Object Spread and Rest, so it is possible now:

var my: MyTest = {test: "hello", newTest: "world"}

var { test, ...reduced } = my;

After that reduced will contain all properties except of "test".

Solution 2:[2]

Another possible approach:

As other answers have mentioned, you can't avoid doing something at runtime; TypeScript compiles to JavaScript, mostly by simply removing interface/type definitions, annotations, and assertions. The type system is erased, and your MyInterface is nowhere to be found in the runtime code that needs it.

So, you will need something like an array of keys you want to keep in your reduced object:

const myTestKeys = ["test"] as const;

By itself this is fragile, since if MyInterface is modified, your code might not notice. One possible way to make your code notice is to set up some type alias definitions that will cause a compiler error if myTestKeys doesn't match up with keyof MyInterface:

// the following line will error if myTestKeys has entries not in keyof MyInterface:
type ExtraTestKeysWarning<T extends never =
  Exclude<typeof myTestKeys[number], keyof MyInterface>> = void;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type 'UNION_OF_EXTRA_KEY_NAMES_HERE' does not satisfy the constraint 'never'

// the following line will error if myTestKeys is missing entries from keyof MyInterface:
type MissingTestKeysWarning<T extends never =
  Exclude<keyof MyInterface, typeof myTestKeys[number]>> = void;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type 'UNION_OF_MISSING_KEY_NAMES_HERE' does not satisfy the constraint 'never'

That's not very pretty, but if you change MyInterface, one or both of the above lines will give an error that hopefully is expressive enough that the developer can modify myTestKeys.

There are ways to make this more general, or possibly less intrusive, but almost no matter what you do, the best you can reasonably expect from TypeScript is that your code will give compiler warnings in the face of unexpected changes to an interface; not that your code will actually do different things at runtime.


Once you have the keys you care about you can write a pick() function that pulls just those properties out of an object:

function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K> {
  return keys.reduce((o, k) => (o[k] = obj[k], o), {} as Pick<T, K>);
}

And them we can use it on your test object to get reduced:

var test: MyTest = { test: "hello", newTest: "world" }

const reduced: MyInterface = pick(test, ...myTestKeys);

console.log(JSON.stringify(reduced)); // {"test": "hello"}

That works!

Playground link to code

Solution 3:[3]

Are you trying to only set/assign properties listed on the interface only? Functionality like that is not available in TypeScript but it is very simple to write a function to perform the behaviour you looking for.

interface IPerson {
    name: string;
}

class Person implements IPerson {
	name: string = '';
}
class Staff implements IPerson {
	name: string = '';
    position: string = '';
}

var jimStaff: Staff = {
    name: 'Jim',
    position: 'Programmer'
};

var jim: Person = new Person();
limitedAssign(jimStaff, jim);
console.log(jim);

function limitedAssign<T,S>(source: T, destination: S): void {
    for (var prop in destination) {
        if (source[prop] && destination.hasOwnProperty(prop)) {
            destination[prop] = source[prop];
        }
    }
}

Solution 4:[4]

In your example newTest property won't be accessible thru the reduced variable, so that's the goal of using types. The typescript brings type checking, but it doesn't manipulates the object properties.

Solution 5:[5]

In a general way, how can you make the 'reduced' variable to only contain the properties declared in the 'MyInterface' interface.

Since TypeScript is structural this means that anything that contains the relevant information is Type Compatible and therefore assignable.

That said, TypeScript 1.6 will get a concept called freshness. This will make it easier to catch clear typos (note freshness only applies to object literals):

// ERROR : `newText` does not exist on `MyInterface`
var reduced: MyInterface = {test: "hello", newTest: "world"}; 

Solution 6:[6]

Easy example:

let all_animals = { cat: 'bob', dog: 'puka', fish: 'blup' };
const { cat, ...another_animals } = all_animals;
console.log(cat); // bob

Solution 7:[7]

One solution could be to use a class instead of an interface and use a factory method (a public static member function that returns a new object of it's type). The model is the only place where you know the allowed properties and it's the place where you don't forget to update them accidentaly on model changes.

class MyClass {
  test: string;

  public static from(myClass: MyClass): MyClass {
    return {test: myClass.test};
  }
}

Example:

class MyTest extends MyClass {
  test: string;
  newTest: string;
}

const myTest: MyTest = {test: 'foo', newTest: 'bar'};
const myClass: MyClass = MyClass.from(myTest);

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3 caesay
Solution 4 Tsvetomir Nikolov
Solution 5 basarat
Solution 6 pablorsk
Solution 7