If you're writing a server in JavaScript, you might write an endpoint that converts an object to JSON:
|
On the client, you might use the fetch
API to hit this endpoint and deserialize (parse) the data:
|
What's the relationship between the user
object in the server and the corresponding user
object in the client? And how would you model this in TypeScript?
Because the serialization and deserialization ultimately happens via JavaScript's built-in JSON.stringify
and JSON.parse
functions, we can alternatively ask: what's the return type of this function?
|
If you mouse over jsonRoundTrip
on the TypeScript playground, you'll see that its inferred return type is any
. That's not very satisfying!
It's tempting to make the return type T
, so that this is like an identity function:
|
But this isn't quite right. First of all, there are many objects which can't be directly represented in JSON. A regular expression, for instance:
|
Second, there are some values that get transformed in the conversion process. For example, undefined
in an array becomes null
:
|
With strictNullChecks
in TypeScript, null
and undefined
have distinct types.
If an object has a toJSON
method, it will get called by JSON.stringify
. This is implemented by some of the standard types in JavaScript, notably Date
:
|
So Date
s get converted to string
s. Who knew? You can read the full details of how this works on MDN.
How to model this in TypeScript? Let's just focus on the behavior around Dates. For a complex mapping like this, we're going to want a conditional type:
|
This is already doing something sensible:
|
We even get support for union types because conditional types distribute over unions:
|
But what about object types? Usually the Date
s are buried somehwere in a larger type. So we'll need to make Jsonify
recursive. This is possible as of TypeScript 3.7:
|
In the case that we have an object type, we use a mapped type to recursively apply the Jsonify
transformation. This is already starting to make some interesting new types!
|
What if there's an array involved? Does that work?
|
It does! How was TypeScript able to figure that out?
First of all, Arrays are objects, so T extends object
is true for any array type. And keyof T[]
includes number
, since you can index into an array with a number
. But it also includes methods like length
and toString
:
|
So it's a bit of a surprise Jsonify
produces such a clean type for the array. Perhaps mapped types over arrays are special cased.
But regardless, this is great! We can even loosen the definition slightly to handle any object with a toJSON()
method (including Dates):
|
Here we've used the infer keyword to infer the return type of the toJSON
method of the object. Try the last example out in the playground. It really does return a number
!
As TypeScript Development lead Ryan Cavanaugh once said, it's remarkable how many problems are solved by conditional types. The types involved in JSON serialization are one of them! If you produce and consume JSON in a TypeScript project, consider using something like Jsonify
to safely handle Dates in your objects.