TypeScript tends to do a very good job of inferring types when you leave off explicit annotations. (Chapter 3 of Effective TypeScript is devoted to this topic.) But when you use tuples or string literal types, this will sometimes go wrong. This post explores using identity functions with carefully constructed type signatures to guide inference towards alternative types.
Say you have a function to calculate the distance between two points:
|
If you define some points and try to call this function, you'll get an error:
|
The issue is that TypeScript has inferred the types of p1
and p2
as number[]
, whereas you would have preferred Point
. These aren't compatible (there are many number arrays that aren't Point
s) and hence the error.
There are a whole class of type errors like this that happen when the inferred type wasn't quite the one you had in mind. In this case the easiest solution is to either declare the type of each point:
|
or use a "const assertion" to avoid inferring the wider types:
|
In this case the inferred types are much narrower than Point
, but they are assignable to Point
, so this type checks.
But in this post I want to talk about a slightly different approach, which is to use identity functions that adjust the inferred type. Here's one way you could use an identity(ish) function to get a Point
:
|
The Point
function is a value, so it's fine that it has the same name as the Point
type, which exists in a separate namespace (Item 8 in Effective TypeScript discusses how to know if a symbol is in type or value space). The ": Point
" declares the return type of the function. This is preferable to as Point
, which would not perform excess property checking.
This certainly solves the problem and is sometimes more convenient than using declarations. Examples of this pattern in the wild include Material-UI's createStyles
and React Native's StyleSheet.create
.
In the case of a tuple, though, there's a neat trick (which I learned from Boris Cherny's Programming TypeScript) to solve this more generally. You can use a generic identity function to get TypeScript to infer a tuple type, rather than an array:
|
If you mouse over p1
or p2
in the playground, you'll see that its type has been inferred as [number, number]
, just like we wanted.
This works with any sort of tuples, including those with mixed types:
|
The tuple
function isn't one you'd write in plain JavaScript (it's shorter to use an array literal), but in the TypeScript context it becomes an extremely useful way to change up the inferred type.
You can apply the same idea if you want to let TypeScript infer the keys of an object but still provide an explicit type for the values. For example:
|
If you try to calculate the distance between two capitals, you'll get the same error as before:
|
(To calculate the actual distance you should use something like turf.distance
.)
Mousing over capitals
, you'll see its type is inferred as {ny: number[]; ca: number[]; ak: number[];}
. You could wrap all the capital locations in tuple
like before, but let's try writing a different identity function to force them all to be Point
s:
|
If you mouse over the type of capitals
now, it's
|
and the types all work out!
So how does withValueType
work? In order to infer the object's type as something different, we first need to capture it in a generic argument, just like we did with tuple
. This is T
, and we want TypeScript to infer it. We also want to explicitly write the value type V
(Point
in this case). Unfortunately you can't have a function with one explicit generic parameter and one inferred parameter. TypeScript will either infer all the generic parameters to a function or none of them, not a mix.
There is a standard workaround for this problem: split the function into two, one with an explicit generic parameter and one with an inferred parameter. Instead of an identity function, we now have a function that returns an identity function. (Note the extra ()
after withValueType
!)
It's important to note that this is quite different than using an index type or Record
, which also fixes the type error:
|
The difference is that accessing an invalid key produces an error when you let TypeScript infer the keys, but not when you use an index type:
|
The inferred type of capitals
is more like Record<'ny'|'ca'|'ak', Point>
than Record<string, Point>
. But because the keys were inferred, you didn't have to write 'ny'|'ca'|'ak'
explicitly.
As a final example, you may find that you want to define an object that has some (but not all) of the fields of another object.
For example:
|
What went wrong? Despite the error on distanceToJupiter
, the problem is with defaults
. Its type is inferred as { color: string; style: string; }
, rather than the narrower value types required for a DisplayValue
.
You might try to solve the problem by using Partial
in the declaration of defaults
:
|
Now the issue is that you've lost the specificity of the keys in defaults.
The Partial<DisplayValue>
type marks all the properties as optional, including color
and style
which you've definitely specified. Hence the error about undefined
. (You can see this error on the playground.)
You can't solve this using withValueType
since the values for color
and style
have different types. Instead, you can craft a slightly different generic identity function. I call this withValueTypesFrom
, since you're taking the value types from some other type (if you have a better name, please suggest it!):
|
It's worth repeating that none of the values have changed here, just the types. This has all the right properties: if you specify an invalid color in defaults
, you'll get an error. If you misspell color
as colour
, you'll get an error. And if you leave out value
or units
in distanceToJupiter
, you'll also get an error.
TypeScript tends to do a very good job of inferring types when you leave off explicit annotations. But when you use tuples or string literal types, this will sometimes go wrong. When this happens, type declarations or const assertions are typically the answer. But if you want to adjust inference in more general or complex ways, typed identity functions give you the flexibility to do so.
Do you have other examples of functions that help with inference? Chime in in the comments! You can find the full code from this example on the TypeScript playground. Some of the examples in this post come from an older post with a very different focus that I wrote for the LogRocket blog.