In a previous post I suggested using intersections as a sort of type-level "as any". I'd personally found this technique useful on a few projects as a way of silencing type errors without losing type safety. Because you often needed to apply this trick two or three times for deeply nested types, it also led me to explore ways of reducing repetition in type-level code.
But when I started reworking that advice into an Item for the upcoming second edition of Effective TypeScript, I learned something interesting: there's usually a better way! TypeScript's
infer keyword can infer quite a lot, and it offers a more direct way to achieve the same goals I talked about in those two posts without the repetition.
Those previous posts were motivated by my crosswalk library, which helps you build and use type-safe REST APIs. Using
infer works in that context, too, but the difference is even more dramatic when we look at an OpenAPI Schema.
Say your API lets you create a user. (It also lets you GET a user, but I'll only show the POST here since this is already verbose.) The OpenAPI Schema might look like this:
You can run this through openapi-typescript to generate TypeScript types:
Accessing these types via the
path structure involves a whole bunch of indexing:
If you want to make a generic POST method, you'll quickly run into some errors:
This error makes sense: TypeScript has no reason to believe that this deep sequence of index operations will work on an arbitrary type.
You can use
infer to extract just what you want out of this structure:
What surprised (and impressed me) was that
infer can see right through the
Record helper, which is a wrapper around a mapped type.
If the endpoint doesn't support
POST requests, you'll get a
never type back. A type error would be better, but hopefully the
never will be enough to alert the caller that something is amiss.
Yikes! Compared to this intersection form, the
infer approach is more direct, easier to read and more closely matches the layout of the data. And if you need to extract multiple pieces of information from a type, it's clear how to do it. This is a nice win.
So is "intersect what you have with whatever TypeScript wants" still a useful technique? You sometimes need to write
& string when passing a type parameter to another generic type that expects a
Check out TypeScript Splits the Atom for a definition of
ExtractRouteParams. The problem is that we expect
keyof API to be a subtype of
string, but strictly speaking all TypeScript knows is that it's a subtype of
string | number | symbol. There's nothing preventing
API from being
string, for example, in which case
keyof API = number. Since
ExtractRouteParams expects a
string, this won't fly.
The best solution would be to tell TypeScript that
API should only have
string keys. But I'm not aware of any way to do that: writing
ApiWrapper<API extends Record<string, any>> results in the same error.
In this case, using an intersection to silence the error still makes sense:
When you're working with nested object types, remember that you can use
infer to extract a particular type from deep inside them.
Image credit: Wikimedia Commons