How to Reduce JavaScript Object to Only Contain Properties from Interface

How to reduce javascript object to only contain properties from interface

It is not possible to do this. The reason being interface is a Typescript construct and the transpiled JS code is empty

//this code transpiles to empty!
interface MyInterface {
test: string;
}

Thus at runtime there is nothing to 'work with' - no properties exist to interrogate.

The answer by @jamesmoey explains a workaround to achieve the desired outcome.
A similar solution I use is simply to define the 'interface' as a class -

class MyInterface {
test: string = undefined;
}

Then you can use lodash to pick the properties from the 'interface' to inject into you object:

import _ from 'lodash';  //npm i lodash

const before = { test: "hello", newTest: "world"};
let reduced = new MyInterface();
_.assign(reduced , _.pick(before, _.keys(reduced)));
console.log('reduced', reduced)//contains only 'test' property

see JSFiddle

This is a pragmatic solution that has served me well without getting bogged down on semantics about whether it actually is an interface and/or naming conventions (e.g. IMyInterface or MyInterface) and allows you to mock and unit test

Is it possible to restrict TypeScript object to contain only properties defined by its class?

I figured out a way, using built-in types available since TypeScript version 3, to ensure that an object passed to a function does not contain any properties beyond those in a specified (object) type.

// First, define a type that, when passed a union of keys, creates an object which 
// cannot have those properties. I couldn't find a way to use this type directly,
// but it can be used with the below type.
type Impossible<K extends keyof any> = {
[P in K]: never;
};

// The secret sauce! Provide it the type that contains only the properties you want,
// and then a type that extends that type, based on what the caller provided
// using generics.
type NoExtraProperties<T, U extends T = T> = U & Impossible<Exclude<keyof U, keyof T>>;

// Now let's try it out!

// A simple type to work with
interface Animal {
name: string;
noise: string;
}

// This works, but I agree the type is pretty gross. But it might make it easier
// to see how this works.
//
// Whatever is passed to the function has to at least satisfy the Animal contract
// (the <T extends Animal> part), but then we intersect whatever type that is
// with an Impossible type which has only the keys on it that don't exist on Animal.
// The result is that the keys that don't exist on Animal have a type of `never`,
// so if they exist, they get flagged as an error!
function thisWorks<T extends Animal>(animal: T & Impossible<Exclude<keyof T, keyof Animal>>): void {
console.log(`The noise that ${animal.name.toLowerCase()}s make is ${animal.noise}.`);
}

// This is the best I could reduce it to, using the NoExtraProperties<> type above.
// Functions which use this technique will need to all follow this formula.
function thisIsAsGoodAsICanGetIt<T extends Animal>(animal: NoExtraProperties<Animal, T>): void {
console.log(`The noise that ${animal.name.toLowerCase()}s make is ${animal.noise}.`);
}

// It works for variables defined as the type
const okay: NoExtraProperties<Animal> = {
name: 'Dog',
noise: 'bark',
};

const wrong1: NoExtraProperties<Animal> = {
name: 'Cat',
noise: 'meow'
betterThanDogs: false, // look, an error!
};

// What happens if we try to bypass the "Excess Properties Check" done on object literals
// by assigning it to a variable with no explicit type?
const wrong2 = {
name: 'Rat',
noise: 'squeak',
idealScenarios: ['labs', 'storehouses'],
invalid: true,
};

thisWorks(okay);
thisWorks(wrong1); // doesn't flag it as an error here, but does flag it above
thisWorks(wrong2); // yay, an error!

thisIsAsGoodAsICanGetIt(okay);
thisIsAsGoodAsICanGetIt(wrong1); // no error, but error above, so okay
thisIsAsGoodAsICanGetIt(wrong2); // yay, an error!

TypeScript - extract interface members only - possible?

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)

How to reduce javascript object to only contain properties from interface

It is not possible to do this. The reason being interface is a Typescript construct and the transpiled JS code is empty

//this code transpiles to empty!
interface MyInterface {
test: string;
}

Thus at runtime there is nothing to 'work with' - no properties exist to interrogate.

The answer by @jamesmoey explains a workaround to achieve the desired outcome.
A similar solution I use is simply to define the 'interface' as a class -

class MyInterface {
test: string = undefined;
}

Then you can use lodash to pick the properties from the 'interface' to inject into you object:

import _ from 'lodash';  //npm i lodash

const before = { test: "hello", newTest: "world"};
let reduced = new MyInterface();
_.assign(reduced , _.pick(before, _.keys(reduced)));
console.log('reduced', reduced)//contains only 'test' property

see JSFiddle

This is a pragmatic solution that has served me well without getting bogged down on semantics about whether it actually is an interface and/or naming conventions (e.g. IMyInterface or MyInterface) and allows you to mock and unit test

Typescript: prevent assignment of object with more properties than is specified in target interface

The other answers here are essentially correct: types in TypeScript are generally open/extendable and can always have properties added; that is, they are not exact types (as requested in microsoft/TypeScript#12936) in which only known properties are allowed to exist. TypeScript doesn't really support exact types in general, although it does treat the types of freshly created object literals as exact types via excess property checks, as you've noticed.

If you really want to forbid a particular property key from a type in TypeScript, you can do this by making the property optional and have its type be never or undefined:

interface IUserSansPassword {
username: string;
email: string;
password?: never; // cannot have a password
}

declare class UserSansPassword implements IUserSansPassword {
username: string;
email: string;
password?: never; // need to declare this also
}

Now UserSansPassword is known not to have a defined password property. Of course now the following is an error:

interface IUser extends IUserSansPassword { // error! 
// Types of property "password" are incompatible
password: string;
}

You can't extend IUserSansPassword by adding a password... if A extends B then you can always use an A instance where a B instance is expected. What you can do is extend a related type, your original IUserSansPassword, which can be computed using the Omit helper type:

interface IUser extends Omit<IUserSansPassword, "password"> {
password: string;
}

declare class User implements IUser {
username: string;
email: string;
password: string;
}

And then the following is an error like you expect:

const userSansPassword: UserSansPassword = new User();
// error, mismatch on "password" prop

Okay, hope that helps; good luck!

Link to code



Related Topics



Leave a reply



Submit