There's a Typescript library called io-ts that can help to strong-type json
data fetched from the server and at the same time provide static typescript typing.
Problem
Imagine we have this line that fetches some data from an endpoint:
const employee = await fetchEmployee();
employee
will probably have the any
type. If we knew the shape of the employee object,
we could create a type and cast employee
:
type Employee {
firstName: string;
lastName: string;
}
const employee = await fetchEmployee() as Employee;
But now we are assuming the shape of employee
, and if it changes in future versions of the backend, it can lead to
annoying runtime errors such as accessing properties on undefined objects, which can be hard to track. We could
validate it with a lib such as ajv, but we wouldn't be able to have a single
source of truth.
Solution
With io-ts we can define a type like this:
import * as t from 'io-ts';
const Employee = t.type({
firstName: t.string,
lastName: t.string,
});
type Employee = t.TypeOf<typeof Employee>;
const employee = Employee.decode(await fetchEmployee());
console.log(employee.firstName);
console.log(employee.foo);
Now employee
is correctly typed, and the shape of the data is validated at runtime.
Implementing the pseudo-code
The lib is great, but the documentation found in the repository's README is a little confusing. The easiest way I found
to just validate data and throw if the shape is incorrect is like this:
import { getOrElse } from "fp-ts/lib/Either";
import { failure } from "io-ts/lib/PathReporter";
const toError = (errors: any) => new Error(failure(errors).join('\n'));
const employee = getOrElse(toError)(Employee.decode(await fetchEmployee()));
if (employee instanceof Error) {
throw employee;
}
console.log('the first name is', employee.firstName)
This steps can be easily extracted to a helper function.