Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript adding "| null" to return type of Promise.all

I have an async TS function that makes a request and casts the response data to a boolean and returns it, but in the calling function VS Code is telling me the return value is boolean | null when I make the call in Promise.all. Here's the code:

The function:

import apiAxios from "../apiAxios";

export default async function doesAssignmentHaveTakes(
  assignmentId: number
): Promise<boolean> {
  const response = await apiAxios.get(`/assignments/${assignmentId}/has-takes`);

  return !!response.data;
}

And the caller:

import React, { FC, useState, useCallback } from "react";
import styled from "styled-components/macro";
import AssignmentForm, {
  Props as AssignmentFormProps,
  Value as AssignmentFormValue
} from "./AssignmentForm";
import useAsyncEffect from "../utils/useAsyncEffect";
import getAssignmentById from "../api/assignments/getAssignmentById";
import doesAssignmentHaveTakes from "../api/assignmentTakes/doesAssignmentHaveTakes";

interface Props extends AssignmentFormProps {
  assignmentId: number;
  onSubmit(value: Value): any;
}

export interface Value extends AssignmentFormValue {
  assignmentId: number;
}

const EditAssignmentForm: FC<Props> = props => {
  const { assignmentId, onSubmit, ...rest } = props;
  const [showEditWarning, setShowEditWarning] = useState(false);
  const [initialValue, setInitialValue] = useState<AssignmentFormValue | null>(
    null
  );

  useAsyncEffect(
    async isCancelled => {
      const [fetchedAssignment, hasTakes] = await Promise.all([
        getAssignmentById(assignmentId),
        doesAssignmentHaveTakes(assignmentId)
      ]);

      if (!fetchedAssignment) {
        // TODO: Alert parent component?
        return;
      }

      const value: Value = {
        assignmentId: fetchedAssignment.id,
        assignment: {
          title: fetchedAssignment.title,
          subTitle: fetchedAssignment.subTitle
        },
        sets: fetchedAssignment.sets
          .map(set => ({
            id: set.id,
            index: set.index,
            questions: set.questions
              .map(question => ({
                id: question.id,
                index: question.index,
                isPractice: question.isPractice,
                questionText: question.questionText,
                inputType: question.inputType,
                questionImage: question.questionImage,
                sampleResponseText: question.sampleResponseText,
                sampleResponseImage: question.sampleResponseImage
              }))
              .sort((a, b) => a.index - b.index),
            learningTarget: set.learningTarget,
            isExampleCorrect: set.isExampleCorrect,
            exampleImage: set.exampleImage,
            character: set.character
          }))
          .sort((a, b) => a.index - b.index)
      };

      if (!isCancelled()) {
        setInitialValue(value);
        setShowEditWarning(hasTakes);
      }
    },
    [assignmentId]
  );

  const handleSubmit = useCallback(
    (value: AssignmentFormValue) => {
      onSubmit({
        ...value,
        assignmentId
      });
    },
    [onSubmit, assignmentId]
  );

  if (!initialValue) {
    // Loading...
    return null;
  }

  return (
    <AssignmentForm
      {...rest}
      initialValue={initialValue}
      onSubmit={handleSubmit}
    />
  );
};

export default styled(EditAssignmentForm)``;

The specific lines with the issue:

  const [fetchedAssignment, hasTakes] = await Promise.all([
    getAssignmentById(assignmentId),
    doesAssignmentHaveTakes(assignmentId)
  ]);

And

setShowEditWarning(hasTakes);

The TS error:

TypeScript error in /Users/james/projects/math-by-example/client/src/components/EditAssignmentForm.tsx(71,28):
Argument of type 'boolean | null' is not assignable to parameter of type 'SetStateAction<boolean>'.
  Type 'null' is not assignable to type 'SetStateAction<boolean>'.  TS2345

    69 |       if (!isCancelled()) {
    70 |         setInitialValue(value);
  > 71 |         setShowEditWarning(hasTakes);
       |                            ^
    72 |       }
    73 |     },
    74 |     [assignmentId]

And some screenshots of the error in VS Code

VS Code type tooltip

VS Code error tooltip

Why does TS add null to the resolved types of Promise.all?

like image 698
SimpleJ Avatar asked Dec 23 '22 20:12

SimpleJ


2 Answers

The solution is to add as const to the array you pass to Promise.all.

Explanation

The problem is not with the typing of Promise.all or a bug in the compiler. The issue is what TypeScript does by default with an array. Consider this:

const q = [1, "a"];

The default type inference for q will be (string | number)[]. Even though you have a number as the first position, and a string as the second, TypeScript infers that all positions can be either a string or a number. If you want TypeScript to treat the array as a tuple and assign to each position the narrowest type possible, you can do:

const q = [1, "a"] as const;

TS will infer a type of readonly [1, "a"] for this array. So q can have only the number 1 in the first position and the string "a" in the second. (It is also readonly but that's a side issue here.) This was introduced in TypeScript 3.4.

Ok, what does this have to do with your case? When you pass your array to Promise.all TypeScript is using the kind of type inference I've shown in my first example. Promise.all sees an array in which each item can take the union of all values that the items can take. If you use as const then the inference will be like the second case I've shown above, and this will be reflected accordingly in the type that Promise.all gets. Again, there's no problem with Promise.all's typing. It works with what it gets: bad typing in, bad typing out.

Here's an illustration (also in the playground):

async function fa(): Promise<string | null> { return "foo"; }

async function fb(): Promise<boolean> { return true; }

async function main(): Promise<void> {
    let a: string | null;
    let b: boolean;

    // Remove this "as const" and the code won't compile.
    [a, b] = await Promise.all([fa(), fb()] as const);

    console.log(a, b);
}

main();
like image 91
Louis Avatar answered Jan 05 '23 07:01

Louis


This has been resolved from ts 3.9+ (release note), upgrade to 3.9 and you will not see this error.

like image 22
undefined Avatar answered Jan 05 '23 05:01

undefined