lucbas
lucbas

Reputation: 867

Typescript: Filter Object by Interface or Class

I would like to sync an object that is provided by an API to a table. The table is defined in Sequelize and has an interface and a class:

declare interface SampleInterface {
  value1?: string;
  value2?: string;
  value3?: number;
}
class SampleClass implements SampleInterface {
  value1?: string;
  value2?: string;
  value3?: number;
}

The response of the API is not always the same, but could look something like this:

const sampleResponse = {
  value2: "..."
  value3: 0
  value4: "..."
}

Now, I would only like create a new object that can be passed to sequelize that has the matching contents, for example:

const filteredResponse = {
  value2: "..."
  value3: 0
}

How can I match the object properties keys to the interface or class?

Thanks!

Upvotes: 4

Views: 5423

Answers (3)

Drag13
Drag13

Reputation: 5988

If I understand you right, you:

Have: API which produces not 100% predictable response.

Want: to create a concrete instance of the class from this untrusted source

If I am right you have two options:

If input object is not very big and dynamic you could do everything explicitly:

const unreliableObject = fetchFromApi();
const result = new Result();

if (typeof unreliableObject.name  === 'string') {
    result.name = unreliableObject.name;
}

This code is more-less OK except it is toooo verbose.

As a bit more advanced solution, you can create TransformationMapper, something like this:

class MyClass {
    name: string;
}

const expectedKeys: (keyof MyClass)[] = ['name'];
const data: any = { v: 1, name: '13212' };

const res = expectedKeys.reduce((result, fieldName) => {

    const value = data[fieldName];
    if (value != null) {
        result[fieldName] = data[fieldName]
    }

    return result;
}, new MyClass());

console.log(res);

UPDATE

Is there any way, I could the get keyof MyClass programmatically

The main idea is to get a scheme to parse the original response. Luckily, you already got it.

So you need to: create an instance of the desired class, and get keys from it:

This can be done using:

Object.keys()

Object.entries()

const data: any = {};

let result = new MyClass();
result = Object.keys(result).reduce((result, fieldName) => {

    const value = data[fieldName];
    if (value != null) {
        result[fieldName] = data[fieldName]
    }

    return result;
}, result)

But I also have to warn you. If you don't trust API, you should not only parse, but also validate values you are parsing. In another case, incorrect types provided via API may break your app.

You can write your own validation (it is not that hard) or take something existing like yup

Upvotes: 3

Kamil Szot
Kamil Szot

Reputation: 17817

You can get fields of class programmatically if you set default values to them and instantiate the class

class SampleClass {
    value1?= "";
    value2?= "";
    value3?= 0;

}
var keys = Object.keys(new SampleClass()) as (keyof SampleClass)[];

var ob = { value1: "asd", value2: "sdad", value4: "xxx" };

var result: SampleClass = {};
for (var k of keys) {
    if (k in ob) { // remove this line if you want set missing fields to undefined
        result[k] = (ob as any)[k];
    }
}

console.log(result);

Upvotes: 3

Kamil Szot
Kamil Szot

Reputation: 17817

I think you need to extend your TypeScript with custom transformer like this one: ts-transformer-keys to be able to get the keys of an interface and the filter your response by copying only those keys.

Plain TypeScript doesn't generally allow pure type information to be passed to runtime code. So you can't see the interface at runtime and you can't know what are its fields to filter them.

You could get property names of a class, but you have to set them in your code:

class SampleClass implements SampleInterface {
  value1?: string = "";
  value2?: string = "";
  value3?: number = 0;
}

var a = new SampleClass();
for (var k in a) {
    console.log(k);  // prints value1, value2, value3
}

Alternatively you could declare interface in this weird roundabout way through enum because enum is one of the TypeScript construct that are accessible at runtime:

enum Props {
    value1 = "value1",
    value2 = "value2",
    value3 = "value3"
};

declare interface SampleInterface {
  [Props.value1]?: string;
  [Props.value2]?: string;
  [Props.value3]?: number;
}

class SampleClass implements SampleInterface {
  value1?: string = "";
  value2?: string = "";
  value3?: number = 0;
}

for (var k in Props) {
    console.log(k);  // prints value1, value2, value3
}

Upvotes: 3

Related Questions