mario595
mario595

Reputation: 3761

Typescript check if object matches type

I have the following problem. Let's say I have a type:

type A = {
    prop1: string,
    prop2: {
        prop3: string
    }
}

I am receiving some json object from an external service and I want to validate if that json matches type A:

function isA(obj:any): boolean {
    // What should be here?
}

So if my obj is something like:

{
 prop1: "Hello",
 prop2: {
    prop3: "World"
 }
}

or

{
     prop1: "Hello",
     prop2: {
        prop3: "World"
     },
     moreProps: "I don't care about"
}

The function would return true, but false for something like

{
     foo: "Hello",
     bar: {
        prop3: "World"
     }
}

What's the easiest way to achieve this?

Thanks.

Upvotes: 17

Views: 24506

Answers (2)

potatoesforsale
potatoesforsale

Reputation: 11

Not a perfect solution either, but another option is to define a "template object" against which you can perform a runtime comparison like this:

// A const object used to define a type and to serve as a template for runtime comparison.
const myTemplateObject = {
  prop1: "",
  prop2: 12,
  prop3: 14 as string | number,
  prop4: {
    potatoes: "",
    carrots: 0,
  },
};

// So you can use the type in the rest of your code. Or just define it explicitly and make the object above an instance of it.
export type myType = typeof myTemplateObject;

export function matchesType(
  object: Record<string, unknown>,
  templateObject: Record<string, unknown>,
) {
  for (const key in templateObject) {
    const templatePropValue = templateObject[key];
    const templatePropType = templatePropValue;
    switch (templatePropType) {
      case "function":
      // fall-through
      case "symbol":
      // fall-through
      case "undefined":
        throw new Error(
          `matchesType function does not support template objects with ${templatePropType} fields`,
        );
      // or return false if you prefer
      case "bigint":
      case "boolean":
      case "number":
      case "string":
        return templatePropType === typeof object[key];
      case "object":
        const objectPropValue = object[key];
        if (typeof objectPropValue === "object" && objectPropValue !== null) {
          return matchesType(
            objectPropValue as Record<string, unknown>,
            templatePropValue as Record<string, unknown>,
          );
        } else {
          return false;
        }
    }
  }
  return true;
}

Upvotes: 1

casieber
casieber

Reputation: 7542

  1. Use a type-guard so that Typescript will also narrow the type at type-checking time for you

To use a type-guard, you should change the return type of your isA function to be obj is A

Overall that should make your type validation function look like:

function isA(obj: unknown): obj is A {
    // return boolean here
}
  1. Use the typeof operator to check properties

typeof will return a string value telling you what the type of a variable is. (docs)

In this case, for A you can do something like:

function isA(obj: unknown): obj is A {
    return (
        obj &&
        typeof obj === 'object' &&
        typeof obj['prop1'] === 'string' &&
        obj['prop2'] &&
        typeof obj['prop2'] === 'object' &&
        typeof obj['prop2']['prop3'] === 'string'
    );
}

It's not the most readable thing in the world, and you could always break it down to it's component pieces and comment each check if you'd like.

One important thing to note, however, is that typeof null is actually 'object' so you can't simply check if typeof obj['prop2'] === 'object' and then move on, you'll need to also check if it exists since it still could be null.

At this point, not only will you be validating correctly at runtime, but TypeScript will now be able to improve its type-checking by narrowing the type of obj to A when isA returns true.

Upvotes: 9

Related Questions