The data you use in your app can come from many sources, some more reliable than others. But regardless of its origin and who produced it, it’s your responsibility to validate and sanitize all the data you get before using it.
Wait! You don’t know what that is? Well, the WordPress documentation already has the answer for you:
Validating input is the process of testing data against a predefined pattern (or patterns) with a definitive result: valid or invalid. On the other hand, Sanitizing input is the process of securing/cleaning/filtering input data. Validation is preferred over sanitization because validation is more specific. But when “more specific” isn’t possible, sanitization is the next best thing.
WordPress Developer Docs
Data validation and sanitation are usually dealt with on the server side of WordPress plugins: when receiving data submitted by a user, the plugin developer has to validate its nonce, make sure all fields are set up correctly, and which ones are not. After all, the server is where all data, sensitive or not, ends up being stored, and therefore it’s paramount to keep it safe.
But, as we move towards more reactive platforms implemented using JavaScript, validating and sanitizing data also becomes important on the client side. And that’s what I’d like to talk about briefly today.
Fetching Data in JavaScript
Let’s consider an example extracted from Nelio Content’s source code. The plugin has Editorial Tasks, a feature that basically lets you assign tasks to members of your team so that everyone knows who has to do what and when. Here’s how they look like:

Editorial Tasks are stored in Nelio’s cloud. This means that, for example, when you toggle the status of a task, our plugin has to trigger a POST request to our cloud to commit the update. The response we get from the cloud is the updated task. Here’s the source code:
export async function markTaskAsCompleted( taskId, completed ) {
const task = select( NC_DATA ).getTask( taskId );
if ( ! task ) {
return;
}
try {
const updatedTask = await apiFetch( {
url: `https://api.neliocontent.com/.../${ taskId }`,
method: 'PUT',
headers: { Authorization: '...' },
data: { ...task, completed },
} );
dispatch( NC_DATA ).receiveTasks( updatedTask );
} catch ( e ) {
showError( e );
}
}Code language: JavaScript (javascript)Which basically translates into the following:
- We define a function named
markTaskAsCompletedthat takes two arguments: ataskIdand whether or not iscompleted. - We search the
taskin our Redux store. If we don’t find it, there’s notaskwhose completion status can be updated. - We then trigger an async PUT request to Nelio’s cloud to update the task. This request sends the task with the new
completedvalue and returns theupdatedTaskas a result. - Once we get the response, we update our Redux store with it.
But, what if the response we got from the server is not an EditorialTask like the one we expect it to be? 🤔
Fetching Data in TypeScript
Well, what if we use TypeScript to address the issue? If TypeScript allows us to define the types of the things we work with, it’ll surely fix the problem, right? Well, let’s rewrite the code into TypeScript:
// Type imported from somewhere else
type EditorialTask = {
readonly id: Uuid;
readonly task: string;
readonly completed: boolean;
readonly postId?: PostId;
// ...
};
export async function markTaskAsCompleted(
taskId: Uuid,
completed: boolean
): Promise< void > {
const task = select( NC_DATA ).getTask( taskId );
if ( ! task ) {
return;
}
try {
const updatedTask = await apiFetch< EditorialTask >( {
url: `https://api.neliocontent.com/.../${ taskId }`,
method: 'PUT',
headers: { Authorization: '...' },
data: { ...task, completed },
} );
dispatch( NC_DATA ).receiveTasks( updatedTask );
} catch ( e ) {
showError( e );
}
}Code language: JavaScript (javascript)This rewrite was extremely easy (assuming all the functions we then use are also written in TypeScript). All we did was add a few types here and there:
taskIdmust beUuidcompletedmust beboolean- Our async function returns a
Promise - We claim that this invokation on
apiFetchwill return anEditorialTask
Wait, what? Claim? What does that even mean?
Well, if we look at the definition of this function on GitHub, we’ll notice some interesting things:
/**
* @template T
* @param {import('./types').APIFetchOptions} options
* @return {Promise<T>} A promise ...
*/
function apiFetch( options ) {
...
}Code language: PHP (php)apiFetch is (at the time of writing this post) written in JavaScript and its types are defined as JSDoc comments. This would be its definition in TypeScript:
import type { APIFetchOptions } from './types';
function apiFetch< T >( options: APIFetchOptions ): Promise< T > {
...
}Code language: JavaScript (javascript)As you can clearly see, this function is generic–that is, a function that takes an optional type T as an argument that it can then be used in its signature to constraints some of its arguments and/or define its result type.
With such a definition, we can basically do absurd things like the following:
declare const args: APIFetchOptions;
const n = apiFetch< number >( args );
// ^? n: Promise< number >
const b = apiFetch< boolean >( args );
// ^? b: Promise< boolean >
const t = apiFetch< EditorialTask )( args );
// ^? t: Promise< EditorialTask >Code language: PHP (php)Using the exact same arguments, one would expect the same result type always. But we can claim the result will be whatever we want it to be: a number, a boolean, an Editorial Task… you name it.
In other words, we’re basically telling the TypeScript compiler “trust me, I know what the result of this invokation will be.”
How can we fix this? Well, just don’t claim anything about the result of apiFetch:
export async function markTaskAsCompleted(
taskId: Uuid,
completed: boolean
): Promise< void > {
const task = select( NC_DATA ).getTask( taskId );
if ( ! task ) {
return;
}
try {
const updatedTask = await apiFetch( {
url: `https://api.neliocontent.com/.../${ taskId }`,
method: 'PUT',
headers: { Authorization: '...' },
data: { ...task, completed },
} );
dispatch( NC_DATA ).receiveTasks( updatedTask );
} catch ( e ) {
showError( e );
}
}Code language: JavaScript (javascript)so that updatedTask is unknown and, therefore, you get an error when trying to use an unknown variable in receiveTasks:
E: Argument of type 'unknown' is not assignable to parameter of
type EditorialTask | readonly EditorialTask[]Code language: JavaScript (javascript)So now, what? Well, we need to implement some logic that checks if the response we received has the appropriate type. With simple types like boolean or number, all we gotta do is typeof x and check if x has the correct type. More complex types would require a type prediate:
const isTask = ( t: unknown ): t is EditorialTask =>
!! t &&
typeof t === 'object' &&
'id' in t &&
isUuid( t.id ) &&
// ...Code language: JavaScript (javascript)and, as you can imagine, the more complex the type, the more complicated the type predicate. That’s until Zod entered the scene, of course….

Nelio Content
I’m so happy about Nelio Content that I will sound like a payed advocate… but here’s why you’ll love it: it works as promised, its auto-scheduling feature is top-notch, Nelio’s value for money is unmatched, and the support team feels like your own.

Panozk
Zod
So far, we’ve seen that, in order to make our TypeScript code safe, we need two things. First, we have to define the EditorialTask type, which we’ll then use in our function declarations, stores, etc. Second, we also need a type predicate that validates if an unknown variable that should be an EditorialTask is indeed an EditorialTask. And, I don’t know about you but, I seems to me that this should be easier… especially when it comes to type validation–the type predicate is horrible to implement!
Zod is a TypeScript-first schema validation with static type inference. In plain English, it means that Zod takes care of the two things we had to do before:
- Define a schema with the shape your data is supposed to have and then use it to validate if said data does indeed have that shape
- Automatically infer the static TypeScript type based on the schema
Let’s rewrite our running example
Honestly, I think that the easiest way to understand all the benefits Zod gives to us developers is by looking at the example we’ve been discussing today. So let’s do that right away:
import z from 'zod';
export const editorialTaskSchema = z.object( {
id: z.string().uuid(),
task: z.string().min( 1 ),
completed: z.boolean(),
postId: z.number().positive().optional(),
} );
export type EditorialTask = z.infer< typeof editorialTaskSchema >;Code language: JavaScript (javascript)Let’s discuss the most important takeaways from the previous snippet:
- We start by defining a new schema using Zod. In this particular instance, the schema is an object, and so we use
z.object. z.objectreceives an object as a parameter which, in our case, describes the shape of anEditorialTask. Each property in this object is, in turn, a Zod schema:idhas to be astringand, moreover, we expect Zod to check if, when validating an unknown object, the string inidis a validuuid.taskis also astring, but in this case all we require is that it’s not empty (i.e., itsminlength is1).completedis aboolean.postIdis anoptional positive number.
- We finally let Zod
inferthe TypeScript typeEditorialTaskby looking at thetypeofour schema.
And that’s it! We now have the exact same pieces we did before, but the result is cleaner and more intelligible. Here’s how we’d use it:
import { editorialTaskSchema } from '@nelio-content/schemas';
export async function markTaskAsCompleted(
taskId: Uuid,
completed: boolean
): Promise< void > {
const task = select( NC_DATA ).getTask( taskId );
if ( ! task ) {
return;
}
try {
const result = await apiFetch( {
url: `https://api.neliocontent.com/.../${ taskId }`,
method: 'PUT',
headers: { Authorization: '...' },
data: { ...task, completed },
} );
const updatedTask = editorialTaskSchema.parse( result );
dispatch( NC_DATA ).receiveTasks( updatedTask );
} catch ( e ) {
showError( e );
}
}Code language: JavaScript (javascript)Easy peasy, right? We simply need to parse the result–if it’s valid, we’ll get the updatedTask as a result and, if it isn’t, it’ll trigger an exception which we can then use. (If you don’t want to work with exceptions, you can use safeParse instead).
And that’s it, my friends. There’s plenty of interesting documentation in Zod’s website. I strongly suggest you take a look at it and consider it using in your projects. It’ll make your life so much easier and you’ll be happier developer no doubt about it.
See you next time!
Featured Image by Fran Jacquier on Unsplash.




Leave a Reply