12

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.

2
  • 1
    The worst abuse I saw of typescript was a class with thisin the constructor: constructor(data: any) { Object.assign(this,data); } - voila - whatever you pass into this class automatically adheres to whatever properties are on the class, according to the compiler....runtime is a different story. Typescript is supposed to catch potential problems at compile time, use it for it's strengths. If you are casting and you don't know for sure what you are casting, and end up writing ifs to check, then you're doing typescript wrong. Commented May 14, 2019 at 0:52
  • If all properties which are declared on interface are not defined in it's concrete class then ideally typescript transpiler throws an error to do same. Ideally you should not check value is present or not with if..else statement. Commented May 14, 2019 at 1:04

2 Answers 2

13

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,
};
Sign up to request clarification or add additional context in comments.

9 Comments

I think that makes sense, but say export interface MyDocument also extended another interface, then would the schema in isMyDocument need to list all those properties, as well? Is it possible to avoid having to relist all those?
Yes. The schema would need to include all of the properties in the extended interface too. You could get around that by using Object.assign(...) or the { ...obj } spread identifier to combine various schemas where appropriate.
Didn't full understand that - where would { ...obj } go?
@userMod2 I edited the question with an example of using { ...obj }.
Thats great - it works. But I have no idea why lol! Why in the child interface do you have name: string;. If that wasn't there, can that whole interface not be removed?
|
2

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.