A man washing his hands

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:

Screenshot of a few editorial tasks as presented in the post editor screen
Editorial Tasks keep track of what and when should be done by whom.

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 markTaskAsCompleted that takes two arguments: a taskId and whether or not is completed.
  • We search the task in our Redux store. If we don’t find it, there’s no task whose 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 completed value and returns the updatedTask as 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:

  • taskId must be Uuid
  • completed must be boolean
  • Our async function returns a Promise
  • We claim that this invokation on apiFetch will return an EditorialTask

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….

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:

  1. 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
  2. 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:

  1. We start by defining a new schema using Zod. In this particular instance, the schema is an object, and so we use z.object.
  2. z.object receives an object as a parameter which, in our case, describes the shape of an EditorialTask. Each property in this object is, in turn, a Zod schema:
    • id has to be a string and, moreover, we expect Zod to check if, when validating an unknown object, the string in id is a valid uuid.
    • task is also a string, but in this case all we require is that it’s not empty (i.e., its min length is 1).
    • completed is a boolean.
    • postId is an optional positive number.
  3. We finally let Zod infer the TypeScript type EditorialTask by looking at the typeof our 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

Your email address will not be published. Required fields are marked *

I have read and agree to the Nelio Software Privacy Policy

Your personal data will be located on SiteGround and will be treated by Nelio Software with the sole purpose of publishing this comment here. The legitimation is carried out through your express consent. Contact us to access, rectify, limit, or delete your data.