Typescript is a nice thing: It lets you define types and make sure your classes and functions adhere to certain expectations. It forces you to think about what data you put into a function and what you will get out of it. If you get that wrong and try to call a function which expects a sting with a - let's say - number, the compiler will let you know. Which is a good thing.
Sometimes this leads to a misconception: I met people who believed typescript would make sure the types are what you say you are. But I have to tell you: Typescript does not do that.
Why? Well, Typescript is working on compiler level, not during the runtime. If you take a look at how the code Typescript produces does look like you'll see it translates to Javascript and strips all the types from the code.
Typescript code:
const justAFunction = (n: number): string => {
return `${n}`
}
console.log(justAFunction)
The resulting Javascript code (assuming you are transpiling to a more recent EcmaScript version):
"use strict";
const justAFunction = (n) => {
return `${n}`;
};
console.log(justAFunction);
It only checks if the types seem to be correct based on your source code. It does not validate the actual data.
Checking types
Is typescript useless then? Well, no, far from it. When you use it right it forces you to check your types if there are no guarantees ("unfortunately" it also provides some easy ways out).
Let's change our example a little bit:
const justAFunction = (str: string[] | string): string => {
return str.join(' ')
}
console.log(justAFunction(["Hello", "World"]))
console.log(justAFunction("Hello World"))
When compiling this will lead to the following error:
index.ts:2:14 - error TS2339: Property 'join' does not exist on type 'string | string[]'.
Property 'join' does not exist on type 'string'.
2 return str.join(' ')
~~~~
Found 1 error in index.ts:2
The compiler forces to think about the type of the variable str
. One solution would be to only allow string[]
into the function. The other is to test if the variable contains the correct type.
const justAFunction = (str: string[] | string): string => {
if (typeof str === 'string') {
return str
}
return str.join(' ')
}
console.log(justAFunction(["Hello", "World"]))
console.log(justAFunction("Hello World"))
This would also translate into Javascript and the type would be tested. In this case we would only have a guarantee that it is a string
and we would only be assuming it is an array.
In many cases this is good enough. But as soon as we have to deal with external data source - like APIs, JSON files, user input and similar - we should not assume the data is correct. We should validate the data and there is an opportunity to ensure the correct types.
Mapping external data to your types
So the first step to solve this problem would probably be to create actual types to reflect your data.
Let's assume the API returns a user record like this:
{
"firstname": "John",
"lastname": "Doe",
"birthday": "1985-04-03"
}
Then we may want to create an interface for this data:
interface User {
firstname: string
lastname: string
birthday: string
}
And use fetch to retrieve the user data from the API:
const retrieveUser = async (): Promise<User> => {
const resp = await fetch('/user/me')
return resp.json()
}
This would work and typescript would recognize the type of the user. But it might lie to you. Let's say the birthday would contain a number with the timestamp (might be somewhat problematic for people born before 1970... but that's not the point now). The type would still treat the birthday as a string despite having an actual number in it... and Javascript will treat it as a number. Because, as we said, Typescript does not check the actual values.
What should we do now. Write a validator function. This might look something like this:
const validate = (obj: any): obj is User => {
return obj !== null
&& typeof obj === 'object'
&& 'firstname' in obj
&& 'lastname' in obj
&& 'birthday' in obj
&& typeof obj.firstname === 'string'
&& typeof obj.lastname === 'string'
&& typeof obj.birthday === 'string'
}
const user = await retrieveUser()
if (!validate(user)) {
throw Error("User data is invalid")
}
This way we can make sure the data is, what it claims to be. But you might see this can get quickly get out of hands in more complex cases.
There are protocols inherently dealing with types: gRPC, tRPC, validating JSON against a schema and GraphQL (to a certain extend). Those are usually very specific for a certain usecase. We might need a more general approach.
Enter Zod
Zod is the missing link between Typescript's types and enforcing the types in Javascript. It allows you to define the schema, infer the type and validate the data in one swipe.
Our User
type would be defined like this:
import { z } from 'zod'
const User = z.object({
firstname: z.string(),
lastname: z.string(),
birthday: z.string()
})
The type could then be extracted (inferred) from this schema.
const UserType = z.infer<User>
and the validation looks like this
const userResp = await retrieveUser()
const user = User.parse(userResp)
Now we have a type and validated data and the code we had to write is only marginally more than without the validation function.
Conclusion
When working with Typescript it is important to know the difference between compiler checks and runtime validation. To make sure external data are conforming to our types we need to have some validation in place. Zod is great tool to deal with exactly that without much overhead and in a flexible way.
Thanks for reading.
Top comments (20)
Nice one!
I use Joi for this kind of validations, I may take a try to Zod to see the differences and which one is more convenient depending on the use-case.
Thank you for sharing! 😁
Joi can do a lot more than Zod (like dealing with linked references, etc.) and is much closer to JSON schemas, this might be a great thing to have for really complex cases. But I'd say Zod being simpler makes it more suitable for many smaller to midsize projects which don't have to deal with that much complexity. (Oh, and Joi is quite a few years older than Zod)
Thanks for commenting and suggesting io-ts. Just looked into it. I see how it works, but in contrast to zod it feels a bit "academic" which might put off some people.
but what if "academic = good, some people = bad"
Well, I think this is an excluding opinion. But sure, if you have the time and resources to teach everyone in your team... more power to you.
To my knowledge & experience, type safety is typically (much) less important in a frontend app than in a backend app.
Meaning: we don't need to obsess about it, or spend effort into it, nearly as much. Relatively speaking. A lot of the static or run-time type checking might be unjustified and overkill.
And then clients wonder why they only see a white screen...
Seriously, yeah, browsers are usually quite forgiving. Which can be a blessing and a curse. For the "curse" side we do have those tools which help us make things more deterministic and make us really think. This helps a lot in creating robust applications with proper error handling by making us aware of some possible edge cases. But you are right, the backend side should (!) be way more rigorous all the time. Unfortunately I saw way to many instances of neglect in the vain of "let's fix that in the frontend"... which is not a good thing to do... but at least the frontend catches those things by being more pedantic than it has to be.
Oh and also... my article wasn't targeting the frontend or backend side specifically.
Hm, works only on classes. This makes it rather limiting (especially for people tending towards a more functional style.)
(Update: Just realized there is also a way to work without those decorators and validate plain objects. But it feels rather cumbersome. I think I prefer the Zod way, and for more complex stuff I'd probably use Joi)
I agree, TypeScript is not actually validating your types. It's simply checking that the types you've written down match the types of the variables you're using. If you assign a string to a variable that's meant to hold an integer, for example, TypeScript will give you a warning, but it won't stop you from running the code.
Good article on a common confusion about TypeScript. I must be missing something here:
Should "t" be "z" here or is it coming from somewhere else?
Oops. Typo. Yes sould be z
This is one of many reasons why at my company we replaced the old TypeScript by the Elm language ... we have a lot of peace and joy coding now 👍😎
While elm is quite a nice language I would be worried using it in production. It being stale since 2019 and being mostly 'maintained' by one person is quite a risk.
Having said that: You'd have the exact same problem in elm, too. Elm is not validating the types in the runtime. Sure the Maybe monad helps but under the hood it still does not make sure an expected string from an external source is not actually - let's say - a number.
Have you try objectmodel ? objectmodel.js.org/
No, never heard of it before. At first glance it seems to be quite interesting, but also trying to do a bit too much for my taste. But it certainly seems to be powerful.
nice point.It's really different between static compiler and dynamic runtime, lots developers confused with them sometimes
Someone really likes their
return
statements, huh? :vor
or, a decent pattern instead of
is
guards:Nice Zod you got there tho, unironically
I can write less verbose code. Easy. But especially when writing an article I tend to be rather be explicit and simple than short and clever. Especially when you think about the scope of an article...
All this to say: I'm usually quite intentional in how I write what.
Nothing about terseness, I just have statement-phobia.