DEV Community

Cover image for You can’t run away from runtime errors using TypeScript
Miguelo Ramírez
Miguelo Ramírez

Posted on • Originally published at dontknowdev.netlify.app

You can’t run away from runtime errors using TypeScript

TypeScript helps a lot when dealing with type errors at compile time. However, form inputs or third-party API responses can cause runtime errors if the types don't match.

This post will explain why runtime errors happen when working with external data sources using TypeScript and what we can do as developers to avoid these problems.

The problem

Let's suppose we fetch employee data from an API. The response's body would look something like this:

{
    id: "1234567890abcdef",
    name: "John Doe",
    birthDate: "2011-10-05T14:48:00.000Z",
    active: true
}
Enter fullscreen mode Exit fullscreen mode

And this would be the code that fetches the data:

type Employee = {
    id: string;
    name: string;
    birthDate: Date;
    active: boolean;
}

const fetchEmployee = async (req: Request): Employee => {
    ...
    await return response.json();
}

const employee = await fetchEmployee(...);
Enter fullscreen mode Exit fullscreen mode

This code would work as it is. It just fetches data from an API and stores it in a variable. However, there is a catch: the Employee type does not match the response. The employee birthDate is not a Date. It is a string.

TypeScript is not aware of these errors until runtime. Typescript will trust the Employee type and suggest methods and properties as if birthDate was a Date. It would be a matter of time before, at any point inside the code, someone could try to do employee.birthDate.getFullYear() and boom: TypeError: getFullYear is not a function.

How to solve this

Assertions

Assertions in Typescript are a great way to ensure data typing. Taking the previous example, we could assert the employee data like this:

const assertIsEmployee = (data: Employee): asserts data is Employee {
    if(typeof data.birthDate.getFullYear !== 'function') {
        throw new AssertionError("Data is not an Employee!");   
    }
}
Enter fullscreen mode Exit fullscreen mode

The usage would be similar to this:

const fetchEmployee = async (req: Request): Employee => {
    ...
    const data = await response.json();
    assertIsEmployee(data);
    return data;
}

const employee = await fetchEmployee(...);
Enter fullscreen mode Exit fullscreen mode

Assertion errors would happen at runtime right after fetching the employee data. E2e tests could detect these before pushing code to production. However, the developer experience is terrible. You might need tests and too much boilerplate to check every possible scenario.

Introducing Zod

Zod is a TypeScript-first schema declaration and validation library. It helps create schemas for any data type and is very developer-friendly. Zod has the functional approach of "parse, don't validate." It supports coercion in all primitive types.

In this case, we could use Zod to create an Employee schema to parse the data from the API. We could use the coerce option on attributes like birthDate and active to coerce them using the primitive types' constructors.

import { z } from "zod";

const EmployeeSchema = z.object({
    id: z.string(),
    name: z.string(),
    birthDate: z.coerce.date(), // Date(input)
    active: z.coerce.boolean(), // Boolean(input)
});

// We can also create a type based on the schema
type Employee = z.infer<typeof EmployeeSchema>;
Enter fullscreen mode Exit fullscreen mode

Now, we could use it easily like this:

const fetchEmployee = async (req: Request): Employee => {
    ...
    const data = await response.json();
    return EmployeeSchema.parse(data);
}

const employee = await fetchEmployee(...);
Enter fullscreen mode Exit fullscreen mode

The parse method still throws errors at runtime when validation fails. But compared to assertion, Zod is way easier to handle as a developer.

Conclusion

We are not safe from runtime errors when using TypeScript. Luckily, there are libraries like Zod that provide solutions to minimize them.

Has this problem ever happened to you? Did you use a similar library to handle it? What has been your experience using Zod? Please let me know and type safely.

Edit: Thanks to @oculus42 for pointing out that you can't do typeof birthdate !== Date. I discovered that typeof birthdate !== 'date' isn't possible either. The most accurate way to check if a variable is a Date would be typeof birthdate.getFullYear === 'function' or something similar. More info on this Stack Overflow question

Top comments (7)

Collapse
 
oculus42 profile image
Samuel Rouse

Thanks for the post! Zod looks like an interesting tool when you have strict schemas (which you really should!).

There is an error in your example in Assertions that uses typeof. typeof always returns a string.

// Current: typeof will never return Date
if(typeof data.birthDate !== Date) {

// Always use string comparisons for typeof
if (typeof data.birthDate !== 'date') {
Enter fullscreen mode Exit fullscreen mode
Collapse
 
miguelo0098 profile image
Miguelo Ramírez

Oops! Thanks for noticing that 😅

Collapse
 
oculus42 profile image
Samuel Rouse

I didn't think about that typeof returning 'object' for Dates! We should be able to use data.birthDate instanceof Date for this. 😬

Collapse
 
lnahrf profile image
Lev Nahar

Are we at the era of trying to fix TypeScript with even more modules?

While Zod looks nice, I think the main issue is that TypeScript is sub-par and does not “work as advertised”.

There is beauty in simplicity, and ensuring types is not a complicated task (unless you are using TypeScript of course).

Collapse
 
efpage profile image
Eckehard

If you are just about parameter checks, you can use this small routine with a nice scheme.

Collapse
 
miguelo0098 profile image
Miguelo Ramírez

Thanks! I'll have a look.

Collapse
 
latobibor profile image
András Tóth

I see people misunderstand TypeScript. In a purpose built statically typed language, you have type enforcement as well: if the object you had did not match the expectation about its shape you get a beefy error.

TypeScript is a superset of JavaScript built for 1) extra checks you can run for your CI/CD pipeline and 2) productivity while using an IDE.

It cannot enforce type. If you use it well you will get way more productive in your IDE than with vanilla-JS.