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
Why does TS add null
to the resolved types of Promise.all
?
The solution is to add as const
to the array you pass to Promise.all
.
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();
This has been resolved from ts 3.9+ (release note), upgrade to 3.9 and you will not see this error.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With