Build a simple object validation utility with TypeScript

TypeScript has become an incredibly powerful language, with a type system that helps developers like you and me build similarly powerful functions, tools, utilities and classes. It's gone from something that was neat and definitely useful in the frontend, to a language that I honestly prefer to use everywhere I possibly can. I've even begun rewriting one my biggest Shopify applications in TypeScript because the type system makes C#'s look like woefully ill-equipped by comparison (even with the new type switching in C# 7).

Since the large majority of my work is building web apps for Shopify -- which you can hire me for, let's talk at joshua@nozzlegear.com -- a lot of my development time involves building out API endpoints that the frontend part of an app can call. I've already covered how you can use a tool called Reinforced.Typings to sync your expected request/response object types from a C# backend to a TypeScript frontend, but if you're using TypeScript on your server too, such a tool is unnecessary (because you can just share your type declarations across the project). Instead the real bulk of the work is in validating the objects and data your endpoints receive.

And that brings us to the point of this post: how can you easily validate objects or types using TypeScript? Yes, a simple if (someProp === xyz) check will work, but if you have dozens of properties on dozens (or potentially hundreds) of expected objects, that will very quickly turn into way too many checks. Moreover, if you add a property to your object model and forget to check for it, you won't get any warnings or help from the TypeScript compiler because you're not checking the shape of the object itself, just its individual properties.

The problem is, just because you're using TypeScript on the frontend and the backend (or syncing types between C# and TypeScript with a tool like Reinforced.Typings), that doesn't mean you don't have to check that an object is what you expect it to be. Mistakes happen, and that property you're expecting to be a boolean might be a falsey 0 or even undefined. Or maybe there's a user out there with an agressive cache, or a user who hasn't refreshed the page since your last update, and their script is still sending data that was valid in a previous version but not in the latest version.

Before we continue on, yes there are plenty of packages out there that will validate objects for you and cover literally everything we'll build in this post and more. This post is largely a learning exercise to show you how you can build such tools, not a post to show how to build something better than those tools.

Setting up the TypeScript types

Before we begin validating, we need to set up a strict return type for all validation functions that will tell us if the value was valid or if it contained an error. Both anonymous functions passed to the validator and the validator classes that we'll build soon will use this return type. We should get the same return type whether we use a validator class or a regular function.

export type Result<T> = 
    | { ok: true, value: T }
    | { ok: false, message: string }

By using a union type for the result, the TypeScript compiler will know which case we're dealing with when we test the ok property. That is to say, if we test result.ok === true then TypeScript knows we have a result.value property too, but if result.ok === false then TypeScript knows there's a result.message instead.

We'll also need to quickly set up a dummy interface for the object we're going to validate throughout the rest of this post. It looks like this:

export interface ExpectedObj {
    foo: string;
    bar: number;
    baz: boolean; 
}

The first pass - if blocks

One of my requirements for validating objects is that the validation mechanism should accept any random function I give it, and then use that function to validate a value. This requirement evolved from using validation packages like Joi, which had (in my opinion) unnecessarily confusing and verbose methods for doing things like "if Property A is true then validate for case X, but if its false then validate for case Y". If you take advantage of union types and literals in TypeScript's type system, there may be cases where you validate differently based on a certain property you receive, and I found that difficult to do in Joi to the point that I would rather just use a function that validates the value itself.

So let's take our first whack at a validation function. It's going to receive a value that we don't know anything about, and it should return that Result<T> type, where T is the ExpectedObj interface:

function validate(obj: any): Result<ExpectedObj> {
    // Foo must be string and cannot be empty
    if (typeof obj.Foo !== "string") {
        return {
            ok: false,
            message: `obj.Foo must be of type string but was ${typeof obj.Foo}`
        }
    } else if (obj.Foo.length === 0) {
        return { 
            ok: false,
            message: `obj.Foo cannot be empty`
        }
    }

    // Bar must be a number, but cannot be negative or greater than 100
    if (typeof obj.Bar !== "number") {
        return {
            ok: false,
            message: `obj.Bar must be of type number but was ${typeof obj.Bar}`
        }
    } else if (obj.Bar < 0) {
        return {
            ok: false,
            message: `obj.Bar must not be less than 0 but was ${obj.Bar}`
        }
    } else if (obj.Bar > 100) {
        return {
            ok: false,
            message: `obj.Bar must be less than or equal to 100 but was ${obj.Bar}`
        }
    }

    // Baz must be a boolean, but not a falsey value like 0 or undefined
    if (typeof obj.Baz !== "boolean") {
        return {
            ok: false,
            message: `obj.Baz must be of type boolean but was ${typeof obj.Baz}`
        }
    }

    return {
        ok: true,
        value: obj
    }
}

This looks pretty simple, if not a little verbose, and it has a few things going for it; chiefly, it's easy to read and easy to reason about. The rules are laid out there in the if blocks and there's nothing magic going on. However, it does have a couple of problems:

  1. There are too many if blocks. Yes, the code above is just fine and there's nothing wrong with it if you're only dealing with one object that has three properties, but this quickly becomes far too verbose and bloated when you apply it to even a "small" API. It easily violates the DRY principle.
  2. Lots of "guarding" must be done to first ensure the properties are actually the type you expect them to be. You don't want your validation functions throwing errors, so you need to first check that obj.Foo is actually a string before you can check its length.

Let's try to clean this up a bit and move all of these checks into their own functions that can be reused. We can also add some improvements by using the TypeScript guard functions, which will tell the TypeScript compiler that a value is of a certain type (e.g. the value is a string) if it passes the function:

function isString(obj: any): obj is string {
    return typeof obj === "string";
}

function isNumber(obj: any): obj is number {
    return typeof obj === "number"
}

function isBoolean(obj: any): obj is boolean {
    return typeof obj === "boolean";
}

const min = (min: number, actual: number) => actual >= min;
const max = (max: number, actual: number) => actual <= max;
const error: <T>(message: string) => Result<T> = message => ({
    ok: false,
    message: message
})

function validate(obj: any): Result<ExpectedObj> {
    // Foo must be a string and cannot be empty
    if (!isString(obj.Foo)) {
        return error(`obj.Foo must be a string but was ${typeof obj.Foo}`);
    } else if (!min(1, obj.Foo.length)) {
        return error(`obj.Foo must not be empty.`);
    }

    // Bar must be a number that is not negative or greater than 100
    if (!isNumber(obj.Bar)) {
        return error(`obj.Bar must be a number but was ${typeof obj.Bar}`);
    } else if (!min(0, obj.Bar)) {
        return error(`obj.Bar must not be negative but was ${obj.Bar}`);
    } else if (!max(100, obj.Bar)) {
        return error(`obj.Bar must not be greater than 100 but was ${obj.Bar}`);
    }

    // Baz must be a boolean
    if (!isBoolean(obj.Baz)) {
        return error (`obj.Baz must be a boolean but was ${typeof obj.Baz}`);
    }

    return {
        ok: true,
        value: obj
    }
}

Again, this works pretty well! However, it can still get a little messy and verbose when you need to combine a bunch of rules though. For example, what if obj.Foo had to: 1) be a string; 2) not be empty; 3) have a length less than 20; and 4) not be equal to "bar"? You quickly balloon into four if blocks and again, this eventually becomes untenable when applied to a "real" API.

Chainable validation functions

What we really need is a validation function that is both chainable and reusable, i.e. you can use notEmpty().maxLength(20).notEquals("bar") -- something that can express all of our rules on one concise line. In the .NET world they call these "fluent" methods, and they're pretty darn easy to build in TypeScript or JavaScript; all you need to do is return the same class (or a new instance of it that preserves the last one's settings) after each function.

To accomplish this, we'll create some Validator classes that will focus on validating one type of value (e.g. string, boolean, number, object). Each Validator class is going to implement an IValidator<T> interface, where <T> is the type that the validator handles. All implementors will have a .go function that accepts a single value (the value that's being validated) and returns the Result<T> interface that we have already declared above:

interface IValidator<T> {
    go(value: unknown): Result<T>;
}

By setting the value type to unknown, the TypeScript compiler will force us to check that the value is exactly the type we want it to be before we can even attempt to validate it. That is to say, if the StringValidator we're about to build wants to validate a string, it must first check that the value is even a string type before anything can be done with it. This helps eliminate entire classes of bugs, because you never know what you're going to get in your validation functions -- that's why you're vlaidating, after all.

Now that we have the interface, we can build our first implementation, a StringValidator. This validator will contain functions for validating string values. It will internally keep track of an array of StringRule objects that will be updated or added to each time one of its rules are invoked. The important bit is that every rule function must return the StringValidator -- this is what makes the functions chainable.

type StringRule = 
    | { type: "equal", value: string }
    | { type: "notEqual", value: string }
    | { type: "minLength", min: number } 
    | { type: "maxLength", max: number }

class StringValidator implements IValidator<string> {
    constructor(private rules?: StringRule[]) {
        if (!Array.isArray(this.rules)) {
            this.rules = [];
        }
    }

    /**
     * Adds a rule to the array of rules, or replaces a rule if it already exists.
     * Use this function to prevent having multiple rules of the same type. 
     */
    private addRule: (rule: StringRule) => StringRule[] = rule => {
        // Filter the current rule set, removing any rule that has the same type of the one being added
        const filtered = this.rules.filter(r => r.type !== rule.type);

        // Add the new rule to the filtered rule array 
        return [...filtered, rule]
    }

    /**
     * Fails if the value being validated is not equal to @param value.
     */
    equals: (value: string) => StringValidator = value => {
        this.rules = this.addRule({ type: "equal", value: value });
        return this;
    }

    /**
     * Fails if the value being validated is equal to @param value.
     */
    notEquals: (value: string) => StringValidator = value => {
        this.rules = this.addRule({ type: "notEqual", value: value });
        return this;
    }

    /**
     * Fails if the string's length is less than @param min.
     */
    minLength: (min: number) => StringValidator = min => {
        this.rules = this.addRule({ type: "minLength", min: min });
        return this;
    }

    /**
     * Fails if the string's length is greater than @param max.
     */
    maxLength: (max: number) => StringValidator = max => {
        this.rules = this.addRule({ type: "maxLength", max: max });
        return this;
    }

    /**
     * Fails if the string is empty.
     */
    notEmpty: () => StringValidator = () => {
        // We don't need to use a specific rule for notEmpty here, we can just set a min length of 1!
        this.rules = this.addRule({ type: "minLength", min: 1 });
        return this;
    }

    /**
     * Fails if the string is not empty. NOTE that an empty string is _not_ the same as a null or undefined value.
     */
    empty: () => StringValidator = () => {
        // Again, we don't need a specific rule for empty, we just set a max length of 0
        this.rules = this.addRule({ type: "maxLength", max: 0 });
        return this;
    }

    /**
     * Checks an individual rule against the value being validated.
     */
    checkRule: (rule: StringRule, value: string) => Result<string> = (rule, value) => {
        throw new Error("Not yet implemented");
    }

    go: (value: unknown) => Result<string> = value => {
        throw new Error("Not yet implemented");
    }
}

With that small ruleset in place, we can set up a switch statement in the .checkRule function and return either an okay result if an individual rule passes, or an error result if it doesn't. Since we used a union type of several different interfaces for the StringRule, the TypeScript compiler will know when the rule has a .max, .min or .value property, complete with intellisense. Depending on your tsconfig.json settings, it will also complain if you forget to check each different case in a switch statement.

checkRule: (rule: StringRule, value: string) => Result<string> = (rule, value) => {
    const err = msg => ({ ok: false, message: msg });
    const ok = () => ({ ok: true, value: value });

    switch (rule.type) {
        case "equal": 
            return rule.value !== value 
                ? err(`Value was expected to be ${rule.value} but was ${value}.`) 
                : ok();

        case "notEqual":
            return rule.value === value 
                ? err(`Value must not be ${rule.value}.`) 
                : ok();

        case "minLength":
            return value.length < rule.min 
                ? err(`String length must be greater than or equal to ${rule.min} but was ${value.length}.`) 
                : ok();

        case "maxLength":
            return value.length > rule.max 
                ? err(`String length must be less than or equal to ${rule.max} but was ${value.length}.`) 
                : ok();
    } 
}

And finally, the last piece of the StringValidator is the .go function. It will loop through each rule and check it, and then short-circuit the loop to return an error result if any rule doesn't pass. If all rules pass, an okay result will be returned and the value will have passed all validation rules!

Remember, also, that the value received by the .go function is unknown, so we have to test if it's a string before we can check each rule.

go: (value: unknown) => Result<string> = value => {
    // Since the value is unknown, we must check that the type is string before validating each rule
    if (value === null) {
        return {
            ok: false,
            message: "StringValidator expected a string but received null."
        }       
    } else if (value === undefined) {
        return { 
            ok: false,
            message: "StringValidator expected a string but received undefined."
        }
    } else if (typeof value !== "string") {
        return {
            ok: false,
            message: `StringValidator expected a string but received ${typeof value}.`
        }
    }

    // TypeScript compiler now knows that value is a string
    // Iterate over all rules and short-circuit to return an error if any rule fails
    for (let rule of this.rules) {
        const result = this.checkRule(rule, value);

        if (result.ok === false) {
            return result;
        }       
    }

    // If none of the rules in the loop had an error, the value passed validation!
    return {
        ok: true,
        value: value
    }
}

And that's the string validator! Since each rule will return itself we can easily set up a chain of multiple rules and, when finished, call validator.go(value) to test the value against them.

Remember that code snippet above, the one with all of the if blocks checking if the string is a string, not empty, has a max length of 20, and is not equal to "bar"? Here's how that's implemented with the StringValidator:

const validator = new StringValidator().notEmpty().maxLength(20).notEqual("foo");
const go = (value: string) => {
    const result = validator.go(value);

    if (result.error) {
        console.error(result.message);
    } else {
        console.log(`String value is valid: ${result.value}.`);
    }
}

go("foo"); // String value is valid: foo.
go("bar"); // Value must not be bar.
go("something longer than 20"); // String length must be less than or equal to 20 but was 24.

Now that's a nice improvement from the if blocks above, although, again, there's nothing wrong with using some simple checks for validation if it fits the bill. In my case, the APIs I build deal with lots and lots of objects, and different versions of those objects when things inevitably change. These utility validation classes save a ton of time and are, as we can see above, pretty simple to build.

Validating object "shapes"

While implementing these validators is fairly straightforward as seen with the StringValidator, I'm not going to implement all of the validators for numbers, booleans, arrays, etc. Instead, let's skip straight to the most useful one: the ShapeValidator for validating the shapes and individual properties of objects.

While the other type validators make testing individual values super simple, it still doesn't help us when properties are added to an object. We get no compiler warning that a new property hasn't been tested or validated. That's where the ShapeValidator comes in! We can have it receive any generic object type, and then set it up to require that every property gets checked. If a new property is added to the type, the TypeScript compiler will give a warning that the property needs to be validated. The same goes if a property is removed from the type.

Rather than having specific validation rules itself, the ShapeValidator will use other validators to check each property. This means that the ShapeValidator can even recursively check child objects by using another ShapeValidator. Personally I also find it very useful to also allow random functions for each property, as there are sometimes cases where I want to change which validator gets used based on one of the properties on the object itself.

Let's start off by defining a type and a guard function. We'll need the following:

  1. A PropertyValidator<T> type -- all of the properties on the validation object will need to be this type. It can either be an IValidator or a function that receives an unknown value and returns Result (the same return type as IValidator<T>.go(value)).
  2. A Shape<T> type, which will take any type (as long as it's an object and not a string, bool, etc.) and morph all of the properties into PropertyValidator<T>. This is what's doing the "magic" for the ShapeValidator, it's going to give us intellisense and compiler protection for every property on the type being validated.
  3. A guard function for working with PropertyValidator<T>, which the TypeScript compiler will use to decide if a value is an IValidator or one of the custom validation functions that returns a result.

Here's what that looks like:

type PropertyValidator<T> =
    | IValidator<T>
    | (value: unknown) => Result<T>;

/**
 * Takes the <T> type and requires all of its properties to be a PropertyValidator.
 */
type Shape<T extends object> = Record<keyof T, PropertyValidator<any>>;

/**
 * Determines whether the value is an IValidator by checking for a .go function.
 */
function isValidator<T>(value: unknown): value is IValidator<T> {
    // Check if the value has a .go function. If so, it's an IValidator
    return typeof (value as IValidator<T>).go === "function";
}

And now we can define the ShapeValidator itself. Instead of adding rules to it, though, it's going to be the only validator that requires an argument: a Shape<T>, which, again, is just the "shape" of the type being validated, but with all properties set to an IValidator or validation function. We'll see an example of that in a minute.

The only other thing that the ShapeValidator will need is the .go function that all other IValidators have. Inside of that function, we'll get the properties on the shape that was passed in and go through them one-by-one, checking the passed in value against each of them.

class ShapeValidator<T extends object> implements IValidator<T> {
    constructor(private shape: Shape<T>) { }

    go: (value: unknown) => Result<T> = value => {
        const err: (msg: string) => Result<T> = msg => ({ ok: false, message: msg });

        // First check that the value is an object and not an array, null, undefined, etc
        if (value === null || value === undefined) {
            return err("Value cannot be null or undefined.");
        } else if (Array.isArray(value)) {
            return err("Value must be an object but was an array.");
        } else if (typeof value !== "object") {
            return err(`Value must be an object but was ${typeof value}.`);
        }

        // Get the keys of both the expected shape, and the value
        const expectedKeys = Object.getOwnPropertyNames(this.shape);
        const actualKeys = Object.getOwnPropertyKeys(value);

        // Check if any expected property is missing from the value
        for (let expected of expectedKeys) {
            if (actualKeys.indexOf(expected) === -1) {
                return err(`Value is missing expected property ${expected}.`);
            }
        }

        // All properties are accounted for! Now loop through each validator on the expected shape and test the value
        for (let expected of expectedKeys) {
            const validator = this.shape[expected];
            const propValue = value[expected];

            // TypeScript doesn't yet know if this is an IValidator, or a function. Use the isValidator guard to check.
            const result = isValidator(validator) ? validator.go(propValue) : validator(propValue);

            // If validation failed, short-circuit the loop and return an error
            if (result.ok === false) return result;
        }

        // All validation passed!
        return {
            ok: true,
            value: value
        }
    }
}

And there's the ShapeValidator! With this, you can easily validate that both the value and the "shape" of the objects you're receiving are what you expect them to be. Here's how you'd use the ShapeValidator in combination with the StringValidator and a custom validation function:

type LineItem = {
    name: string;
    manufacturer: string;
}

type PurchaseRequest = {
    token: string;
    line_item: LineItem;
    /**
     * Customer has the option to tell us a date by which they need the order.
     */
    needed_by: string | null;
}

const validator = new ShapeValidator<Expected>({
    token: new StringValidator().notEmpty(),
    line_item: new ShapeValidator<LineItem>({
        name: new StringValidator().notEmpty(),
        manufacturer: new StringValidator().notEmpty()
    }),
    needed_by: (value: unknown) => {
        if (value === null) {
            return { ok: true, value: null };
        }

        // Value is not null, so use a string validator to validate it.
        return new StringValidator().notEmpty().go(value);
    }
});

Now you can use that validator instance to validate any value under the sun, including arrays, strings, null, undefined, and so on. The validation will only pass if it's an object that has the exact properties you passed in when creating the validator:

validator.go({
    token: "token",
    line_item: {
        name: "Widget",
        manufacturer: "Foo Co."
    },
    needed_by: null
}); // { ok: true, value: ... }

validator.go({
    token: "token",
    line_item: {
        name: "Widget",
        manufacturer: "Foo Co."
    },
    needed_by: null
}); // { ok: true, value: ... }

validator.go({
    token: "token",
    needed_by: null
}); // { ok: false, message: "Value is missing expected property line_item." }

validator.go("foo"); // { ok: false, message: "Value must be an object but was string." }

Here's the best part: since it's pure TypeScript (er, JavaScript) with zero node dependencies, you can even use this same validation utility in the browser before you even send the request!

Beware: only return the values you expect

While the ShapeValidator is now fully functional and ready to go, there's one last thing you should know about before you put this into production: the ShapeValidator is returning exactly what it's given if the object passes all validation. That doesn't sound likea problem at first glance, and in many cases it's not, but I've been bitten by this once before.

Imagine a scenario in which you've built an app that uses a document-style database such as MongoDB or CouchDB, and you have an API set up in front of them. Like a good developer, you're validating all of the input you're receiving before you put data in the database with the ShapeValidator that we just built above. But, because the ShapeValidator is actually returning the exact value it's been given, you end up unknowingly dumping data in your database that you didn't intend!

const myData = validator.go({
    token: "token",
    line_item: {
        name: "Widget",
        manufacturer: "Foo Co."
    },
    needed_by: null,
    something_evil: "A nefarious value that you wouldn't expect to find in your database! egads!"
});

if (myData.ok) {
    await database.put(myData.value);
}

Uh-oh! You've just inserted the something_evil property into your database alongside the properties you validated. Granted, this is a fabricated scenario, and in many cases you're probably transforming the request data into something else before storing it in your database, but this is still a problem that you need to watch for.

Luckily, the ShapeValidator can easily be adjusted to only return the properties that you're expecting (i.e. the ones that you're validating). All we need to do is change ShapeValidator.go to use a reducer function rather than a loop when iterating over the validators. If a property passes validation, you tack it onto the output object and return it once all properties have been looped through.

go: (value: unknown) => Result<T> = value => {
    ....

    // Get the keys of both the expected shape, and the value
    const expectedKeys = Object.getOwnPropertyNames(this.shape);
    const actualKeys = Object.getOwnPropertyKeys(value);

    // Check if any expected property is missing from the value
    for (let expected of expectedKeys) {
        if (actualKeys.indexOf(expected) === -1) {
            return err(`Value is missing expected property ${expected}.`);
        }
    }

    // All properties are accounted for, now reduce over each validator and add it to the output value if it passes
    const defaultState = { ok: true, value: {} }
    const output = expectedKeys.reduce<Result<T>>((state, key) => ({
        // Check if value has failed validation at any point. If so, short-circuit and return the error.
        if (state.ok === false) return state;

        const validator = this.shape[key];
        const propValue = value[key];

        // TypeScript doesn't yet know if this is an IValidator, or a function. Use the isValidator guard to check.
        const result = isValidator(validator) ? validator.go(propValue) : validator(propValue);

        // Return the error if validation failed
        if (result.ok === false) return result;

        // Otherwise, tack the value onto the state object
        return {
            ok: true,
            value: {
                ...state.value,
                [key]: result.value
            }
        }
    }, defaultState)

    return output;
}

There we go, the ShapeValidator is officially complete! Now it will only return the exact properties that it's expected to validate. As a sidenote, this is kind of looping and reducing is one of my favorite parts of functional programming, and is very prevalent in more functional languages like F#. It's such a joy that we can use functional paradigms in TypeScript/JavaScript.

As always, I'm available for freelancing or consulting engagements not only with functional languages like TypeScript, JavaScript and F#, but also more object-oriented languages like C#, Java and Dart! If you're looking for a developer to build your web app, Shopify app, mobile app or otherwise, send me an email at joshua@nozzlegear.com and let's talk!


Learn how to build rock solid Shopify apps with C# and ASP.NET!

Did you enjoy this article? I wrote a premium course for C# and ASP.NET developers, and it's all about building rock-solid Shopify apps from day one.

Enter your email here and I'll send you a free sample from The Shopify Development Handbook. It'll help you get started with integrating your users' Shopify stores and charging them with the Shopify billing API.

We won't send you spam. Unsubscribe at any time.