Schema Validation in TypeScript
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:
- there is no distinct type for
null, it is considered"object", - there is no type for arrays, they are considered
"object"s, - Map, Set, and other similar data structures are also considered
"object"s.
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 }); //trueNotice 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); //trueNote that:
- even though
myObjectInstanceandmyOtherInstanceare structurally the same,myOtherInstanceis not an instance ofMyObject, - both
myObjectInstanceandmyOtherInstanceare instances ofObject.
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)); //falseA 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.
