About MeProjectsBlog

Schema Validation in TypeScript

4 min read

As TypeScript does not validate types at runtime and removes type information at compile time, we often need a way to validate the schema of an object. For example, to check user input or ensure that an API response has the shape we expect.

In this article, I will explain how you can validate objects in TypeScript, what the gotchas are, and the best practices.

typeof, in, instanceof

The typeof, in, and instanceof operators are native to JavaScript and can be a good start for validating objects.

typeof

The typeof operator returns a string indicating the type of the operand’s value.1

console.log(typeof "string"); //"string" console.log(typeof 1); //"number" console.log(typeof 1n); //"bigint" console.log(typeof true); //"boolean" console.log(typeof {}); //"object" console.log(typeof null); //"object" console.log(typeof undefined); //"undefined" console.log(typeof myFunction); //"function" console.log(typeof Symbol()); //"symbol"

It’s worth mentioning that:

in

The in operator returns true if the specified property is in the specified object or its prototype chain.2

console.log("id" in {}); //false console.log("id" in { id: "id" }); //true console.log("id" in { id: null }); //true console.log("id" in { id: undefined }); //true

Notice that it returns true even if the value is null or undefined.

instanceof

The instanceof operator tests to see whether the prototype property of a constructor appears anywhere in the prototype chain of an object.3

This means that an object is only considered an instance of a type if it was created with the new operator.

function MyObject(id) { this.id = id; } const myObjectInstance = new MyObject("id"); const myOtherInstance = { id: "id" }; console.log(myObjectInstance instanceof MyObject); //true console.log(myOtherInstance instanceof MyObject); //false console.log(myObjectInstance instanceof Object); //true console.log(myOtherInstance instanceof Object); //true

Note that:

Type guards

The operators mentioned above are useful until you have more complex schemas, more data types, or uncertainty about how an object from an external library was instantiated. TypeScript provides something called a type guard. A type guard is an expression that performs a runtime check that guarantees a type within a certain scope.4

type TypeA = { fieldA: string }; type TypeB = { fieldB: string }; // Type guard function isTypeA(obj: unknown): obj is TypeA { return (obj as TypeA).fieldA !== undefined; } const objA: TypeA = { fieldA: "fieldA" }; const objB: TypeB = { fieldB: "fieldB" }; console.log(isTypeA(objA)); //true console.log(isTypeA(objB)); //false

A type guard is a normal function, meaning you can check anything you want, for example, minimum and maximum values, or regular expressions for formats such as email addresses.

Using validation libraries

Type guards are powerful tools but take time to write, while most of us only need very generic checks that have been implemented many times before.

The best practice is to use Zod for validation.

Zod is a TypeScript-first validation library. Using Zod, you can define schemas that validate data ranging from simple strings to complex nested objects.5

import * as z from "zod"; const User = z.object({ name: z.string(), }); const input = { name: "name" }; const data = User.parse(input); console.log(data.name);

Zod has validators for simple use cases, like making a field optional or defining minimum string length, as well as more advanced ones such as validating JWT tokens or ISO dates. See all available options in the Zod documentation.

Custom formats

If you can’t find the validator you need, you can create a custom one.

Custom string formats

If the type is a string, you can use the stringFormat function:

const myId = z.stringFormat("my-id", (val) => { // Use any custom validation here return val.length === 100 && val.startsWith("my-"); }); // Or use regex z.stringFormat("my-id", /^my-[a-z0-9]{95}$/);

It is important not to throw an error, but instead return false to indicate that the validation failed.

Custom types not supported by Zod

If the type is not a string, you can use the custom function:

const px = z.custom<`${number}px`>((val) => { return typeof val === "string" ? /^\d+px$/.test(val) : false; }); type px = z.infer<typeof px>; // `${number}px` px.parse("42px"); // "42px" px.parse("42vw"); // throws;

The main difference between stringFormat and custom is that stringFormat will produce an invalid_format issue, which is more descriptive. However, you can always provide custom error messages.

Summary

To summarize, we now understand how schema validation works in TypeScript. We explored the solutions available in JavaScript, looked at what TypeScript adds on top, and learned about an industry-standard validation library that builds on these concepts.

Footnotes

  1. MDN - typeof 

  2. MDN - in 

  3. MDN - instanceof 

  4. TypeScript Docs - User-Defined Type Guards 

  5. Zod Docs 

Was this helpful? Support my work with a coffee!

Buy Me A Coffee