Typescript: How do you provide types for data from external sources?
We all love Typescript because it brings some sanity when working with Javascript. We define our intention (type annotation), and Typescript checks the code we write against our intentions. This works great for static data within our control, but what about data from sources outside our control, i.e., HTTP requests, files, etc.?
In cases like this, Typescript has no way of knowing what the data type is. It is up to us as developers to do the work, and help Typescript understands data from external sources and the types of our external data.
Let’s look at ways in which we can accomplish this:
Type Assertions
One of the most common approaches most developers result in is assertions. By default, data from external sources is typed as any
type. With assertions, you can inform Typescript what the data is, and Typescript will trust you on that.
So, for instance, if we are fetching data from an HTTP API using fetch, the response is inferred as any, as shown below:
const res = await fetch("")
const data = (await res.json())
The above isn’t useful, so we can assert on results, so we can have much better autocompletion and Type Safety moving forward:
const res = await fetch("")
const data = (await res.json()) as Record<string, string>
This assumes we have done our due diligence and ensured that the data we have in our hands is of Record<string, string>
. And If it is not, we are working with the wrong data shape and likely introducing a bug within our system.
TIP: You should only use type assertions sparingly and only when you have more information than Typescript.
I don’t want to go into details, but I previously covered assertions and why you should avoid them here. It suffices to say; that I think we can do better; let’s look at the alternative:
Type Narrowing
Type narrowing is moving a type from a less precise type to a more precise one, for instance, any/unknown to string.
const x: any = "Hello World";
if(typeof x === "string") {
console.log(x)
}
In the above example, we are using Type Guards to check the type of a variable, and Typescript will automatically use the control flow logic to narrow the type of variable x from a broad type any
to a more narrow type of x.
There are several ways to do type narrowing in Typescript, and I have previously covered them here.
Type Narrowing using Custom Type Guards
One approach I had in mind while crafting this issue was to do with Custom Type Guards. Custom Type Guards enable us to create functions that return a type predicate, which hints to Typescript the type of the function parameter. A predicate takes the following format: parameterName is Type
, where parameterName
must be the name of a parameter in the function parameter signature.
Let’s create a custom type guard function that will check if an object is of Type person.
We will start by defining our person Type:
type Person = {
name: string;
age: number;
}
Next, we can create a custom type guard function that returns a type predicate as shown below:
function isPerson(input: any): input is Person {
// implementation
}
Our function just needs to return either true
or false
, with true
indicating that our input is of type Person and false
, the opposite. You need to ensure you have done exhaustive checks on the input before returning the object; in our case, we will ensure that the input has both age
and name
.
function isPerson(input: any): input is Person {
if("name" in input && "age" in input) {
return true
}
return false
}
And then, we can use our type guard as shown below:
if(isPerson(input)) {
const x = input;
}
I covered how to create custom-type guards here.
Schema Validation Libraries
The second alternative to the assertion is using schema validation libraries such as Zod or io-ts. This help validates your data from an external source at runtime while helping Typescript understand the shape of the data you are working with. In this issue, I want to focus on Zod, which I have ample experience with.
Zod
Zod is designed to be as developer-friendly as possible. The goal is to eliminate duplicative type declarations. With Zod, you declare a validator once and Zod will automatically infer the static TypeScript type. It's easy to compose simpler types into complex data structures.
With Zod, you start by defining the shape of your data and possible errors, and then you can validate the data against the schema before using it. Zod can validate anything from a simple string to a very complex object.
An example schema for a simple string:
const simpleStringSchema = z.string();
const person = simpleStringSchema.parse("hello");
// ^? const person: string
An example schema for an object:
const personSchema = z.object({
name: z.string(),
age: z.optional(z.number()),
});
const person = personSchema.parse({ name: "John" });
// const person: { age?: number | undefined; name: string; }
And you can get types from the schema to use for type annotation, as shown below:
type SimpleString = z.infer<typeof simpleStringSchema>;
// type SimpleString = string
type Person = z.infer<typeof personSchema>;
// type Person = { age?: number | undefined; name: string; }
For more information, check out the docs in Zod.
Example
Let’s see an example of using Zod to validate data. I have this VS Code Extension - NPM Imported Package Links - that shows you links to NPM, Github/Github and Opens an Issue for any ESM imports from NPM in your files. One of the features is showing links to NPM for an NPM Package listed on package.json
dependencies (either in the dependencies
, devDependencies
, or peerDependencies
list).
I needed a way to validate that the file I am working with is a valid package.json
, so I started by creating a schema for the package.json, only including fields I care about. In my case, this was dependencies
, devDependencies
and peerDependencies
which are optional, but when available, they need to be a key-value map.
const partialPackageJSONSchema = z.object(
{
dependencies: z.optional(z.record(z.string())),
devDependencies: z.optional(z.record(z.string())),
peerDependencies: z.optional(z.record(z.string())),
}
);
Zod provides you with helper functions to compose the schema, such as optional, record, and string, as shown above. With the above, I can quickly validate the content of package.json, as shown below:
const packageJSONContent = partialPackageJSONSchema.parse(
JSON.parse(document.getText())
);
And Typescript will automatically infer the validated object automatically, as Zod will throw an error if the schema fails validation.
You can find the complete code for the above here.
Final Thoughts
In this post, we looked at working with external data in Typescript and how we can validate and provide types for Typescript. How we handle this is essential as it can eliminate a potential source of bugs that may come from our types matching up to the data we are getting from external sources.
We looked at how we can use Type Narrowing and third-party schema validation libraries to check the shape of the data we are checking, with some real-world examples using Zod.
Now, I want to hear from you; how do you handle external data in your application? Do you do any validation or just assert the types?
And that’s it from me, I hope you enjoyed ❤️ this issue as much as I did writing it, and I hope you have a fantastic week ahead.
Please consider doing one of the following things if you love this issue.
1. Share it ❤️ - Help spread word of mouth and help All Things Typescript grow:
2. ✉️ Subscribe to this newsletter. You will receive weekly content like this from All Things Typescript that will help you learn and master Typescript.
3. Consider Sponsoring me on GitHub ❤️; you will be helping me create more free content for the benefit of everyone.
Au revoir 👋🏿.