Last year I wrote about
Dates, which get converted to strings in this process:
If you use a type on your server,
Jsonify tells you what that type will look like on your client:
First, I didn't come up with this. In fact, Anders presented it at his keynote at the original TSConf, in 2018. He, in turn, based it on discussion on GitHub. What's surprising is that I was at that talk! But it must have gone in one ear and out the other. In any case,
Jsonify became much more compelling once TypeScript 3.7 introduced recursive type aliases in Nov. 2019.
Second, I've learned that
Jsonify isn't a good idea.
Why not? While it's neat that you can model a transformation like this in the type system, it wound up being quite annoying when my team put it into practice.
For example, if you have an API on the server that produces a
Student, then your client-side code should work in terms of
As you pass the
student object around your application, you'll get long error messages any time you forget the
Nothing about this error says "you forgot
Jsonify." We just had to learn "any time you see something about
string, it means you forgot a
Jsonify somewhere." In more realistic code, there can be many
Date objects that are deeply nested, leading to even longer, more confusing errors. These
Date fields typically weren't used by the function producing the error, so we weren't even gaining safety for our trouble.
Jsonify for a few months, we decided to get rid of it by eliminating
Dates from our API. Most of these were coming from our database. By default,
node-postgres converts Postgres date/timestamp columns to
Date objects. This makes a lot of sense as a default. But to keep our server and and client types equal, we decided to just use strings instead.
To make this work, we had to reconfigure the types returned by node-postgres:
With these changes in place, our API types were fully unified: they were exactly the same on the server and the client:
So we could drop
Jsonify! Now we could write code like this error-free:
The lesson here is that you should prefer to unify your types rather than model small differences between them. By unifying your types, you'll save all the time and effort you would have spent getting the transformations exactly correct and applied in exactly the right places.
As another example, it's common to have
snake_case column names in your database and convert them to
camelCase for JS/TS variable names and types in your API:
As I discussed in my TypeScript Splits the Atom post, as of TypeScript 4.1 you can model this snake_case → camelCase transformation in the type system. But should you? Following the mantra of "unify rather than model small differences," clearly you should not! In this case you could either use
snake_case names in your API or use a tool like pg-camelcase to convert the
snake_case names to
camelCase as you load them from the database (you can do something similar with knex). In either case, you'll be able to forget about the type transformations entirely.
Of course, any rule comes with caveats.
First, this isn't always an option. You may need the two types if the database and the API aren't under your control. If this is the case, then modeling these sorts of differences systematically in the type system will help you find bugs in your transformation code. It's better than creating types ad-hoc and hoping they stay in sync.
Second, don't unify types that aren't representing the same thing! Say you have a tagged union, for example:
It would be counterproductive to "unify" these types:
This will make TypeScript less effective at finding bugs in your
Response-handling code. The two
Response types are fundamentally different, so they should not be unified. This rule is best applied to types that are fundamentally the same but superficially different.
If you find yourself creating lots of types that are only slightly different from one another, consider unifying them. You'll be happy you did!