Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript - Check an object has all interface properties

Say I have some interface:

export interface MyDocument {
    id: string,
    collection: string[];
}

I then create a new (well cast an existing to that type):

const workingDocument = <MyDocument>document;

And finally, I have this if statement block to check whether it actually contains everything I have specified in that interface:

if (!workingDocument.id) {
   throw new Error("Document missing `id`");
} else if (!workingDocument.collection) {
   throw new Error("Document missing `collection` array");
}

However I don't seem to like this as that if statement could grow forever and not very good to maintain.

Is there a better way?

Thanks.

like image 814
userMod2 Avatar asked May 14 '19 00:05

userMod2


2 Answers

Original Answer

If I understand correctly, you're asking for a runtime check that an object includes all the properties that an interface defines. That is not possible with an interface on its own, because the type information associated with an interface does not make it to runtime; in other words, the interface is only useful when we run the TypeScript compiler.

What you could do is to create an schema that contains all the properties of the interface. Then you could loop over that schema to check that all the properties exist on your object. Here is an example of how that might look. I have wrapped the example in a user-defined type guard.

export interface MyDocument {
    id: string,
    collection: string[];
}

const isMyDocument = (input: any): input is MyDocument => {

    const schema: Record<keyof MyDocument, string> = {
        id: 'string',
        collection: 'array'
    };

    const missingProperties = Object.keys(schema)
        .filter(key => input[key] === undefined)
        .map(key => key as keyof MyDocument)
        .map(key => new Error(`Document is missing ${key} ${schema[key]}`));

    // throw the errors if you choose

    return missingProperties.length === 0;
}

const obj = {};

if (isMyDocument(obj)) {
  // the compiler now knows that obj has all of its properties
  obj.collection;
} 

Here is the above code in the TypeScript playground.

Answer to Question in the Comments

Here is how you might use the ... operator to extend a schema.

interface ParentDocument { 
    id: string,
    collection: [],
}

interface ChildDocument extends ParentDocument { 
    name: string;
}

const schemaParent: Record<keyof ParentDocument, string> = {
    id: 'string',
    collection: 'array'
};

const schemaChild: Record<keyof ChildDocument, string> = {
    name: 'string',
    ...schemaParent,
};
like image 166
Shaun Luttin Avatar answered Sep 27 '22 21:09

Shaun Luttin


If you're creating / using this Document type internally, you can use types/interfaces to assert their types - for yourself - without the need to cast.

However, if the documents are from outside of your typescript app, you'll need to result to some form of manual type guarding / checking (the thing you want to avoid).

like image 41
Tobiq Avatar answered Sep 27 '22 22:09

Tobiq