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.
My previous posts would have suggested this chain of helpers instead:
|
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 string
:
|
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 PropertyKey
, aka 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