Inferring Object and Function Types in TypeScript
TypeScript’s powerful inference helps us avoid bloating our code with lots of type annotations. The typeof
keyword can help us when we want to strongly-type a variable from another variable’s type.
Let’s go through an example where this is useful in React.
Getting the type of an object
Here’s a snippet of a strongly-typed form in React:
const defaultState = { name: "", email: "" };
const App = () => {
const [values, setValues] = React.useState(defaultState); const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
save(values); };
...
};
The values
state is inferred to be of type { name: string, email: string }
from the defaultState
variable.
In handleSubmit
, we call a save
function outside the component which will be something like:
const save = (data) => {
// post the data to the server ...
};
We are in TypeScript land, so we need to give the data
parameter a type annotation. Does this mean we now have to create a type for this? Fortunately, we can use the inferred type of defaultState
using the typeof
keyword:
const save = (data: typeof defaultState) => {
// post the data to the server ...
};
You can see this example in CodeSandbox at https://codesandbox.io/s/typeof-uy93d?file=/src/index.tsx;
Getting the return type of a function
Sometimes it’s useful to get the return type of a function.
An example is in Redux code, where we want to create a union type for all the actions for use on the action
reducer function parameter. So, if we have the following action creators:
function addPerson(personName: string) {
return {
type: "AddPerson",
payload: personName,
} as const;
}
function removePerson(id: number) {
return {
type: "RemovePerson",
payload: id,
} as const;
}
… we want to create a union type as follows:
type Actions =
| return type of addPerson
| return type of removePerson
This isn’t valid syntax, but hopefully you get the idea of what we are trying to do.
Luckily there is handy utility type called ReturnType
that we can use:
type Actions =
| ReturnType<typeof addPerson>
| ReturnType<typeof removePerson>;
This gives us exactly the type that we require:
{
readonly type: "AddPerson";
readonly payload: string;
} | {
readonly type: "RemovePerson";
readonly payload: number;
}
Nice!
Getting the return type of an asynchronous function
What if the functions are asynchronous?
async function addPersonAsync(
personName: string
) {
await wait(200);
return {
type: "AddPerson",
payload: personName,
} as const;
}
async function removePersonAsync(id: number) {
await wait(200);
return {
type: "RemovePerson",
payload: id,
} as const;
}
type ActionsAsync =
| ReturnType<typeof addPersonAsync>
| ReturnType<typeof removePersonAsync>;
The ActionsAsync
type isn’t quite what we require:
Promise<{
readonly type: "AddPerson";
readonly payload: string;
}> | Promise<{
readonly type: "RemovePerson";
readonly payload: number;
}>
What we want is the type after the promise is resolved.
Let’s look at the definition of ReturnType
:
type ReturnType<
T extends (...args: any) => any
> = T extends (...args: any) => infer R
? R
: any;
This looks a bit tricky, so, let’s break it down:
- The type is a generic type where the parameter has the signature
T extends (...args: any) => any
. i.e. we need to pass a function type or a type error will occur. - The type is a conditional type with the condition being whether the parameter has a function signature.
infer R
is the valuable bit in the condition because this puts the return type from the generic parameter into aR
parameter.- If the condition is
true
(i.e. the generic parameter is a function) thenR
is returned (i.e. the function’s return type). any
is returned if the condition isfalse
.
This explains why we are getting the Promise
included in our asynchronous function return type.
Let’s have a go at creating our own utility type for the return type of an asynchronous function:
type ReturnTypeAsync<
T extends (...args: any) => any
> = T extends (...args: any) => Promise<infer R>
? R
: any;
So our Actions
union type becomes:
type ActionsAsync =
| ReturnTypeAsync<typeof addPersonAsync>
| ReturnTypeAsync<typeof removePersonAsync>;
This gives the type we expect:
{
readonly type: "AddPerson";
readonly payload: string;
} | {
readonly type: "RemovePerson";
readonly payload: number;
}
If we want to be clever and improve ReturnTypeAsync
so that it works with asynchronous functions as well as synchronous ones, we can do it as follows:
type ReturnTypeAsync<
T extends (...args: any) => any
> = T extends (...args: any) => Promise<infer R>
? R
: T extends (...args: any) => infer R
? R
: any;
Neat!
You can see this example in CodeSandbox at https://codesandbox.io/s/function-return-type-v44m2?file=/src/index.ts
If you to learn more about using TypeScript with React, you may find my course useful: