Carl Rippon

Building SPAs

Carl Rippon
BlogBooks / CoursesAbout
This site uses cookies. Click here to find out more

Inferring Object and Function Types in TypeScript

June 09, 2020
typescriptreact

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 a R parameter.
  • If the condition is true (i.e. the generic parameter is a function) then R is returned (i.e. the function’s return type).
  • any is returned if the condition is false.

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

Learn TypeScript

NEW!🎉LIMITED LAUNCH DISCOUNT

An interactive course for JavaScript developers who want to learn modern TypeScript

  • Learn to use TypeScript's amazing type system with your existing JavaScript skills to boost your productivity
  • Over 70 interactive tutorial style lessons
  • Quizzes in each chapter to reinforce knowledge
  • Covers beginner topics through to advanced
Learn TypeScript
Find out more