Issue
How can I conditionally make type annotations on variables. I’ve got a function with an argument that is changing the behaviour of an api call. The behaviour is changed in such a way that the response differs. I want to type that but wasn’t able to to do so after hours of research.
Thats the situation ("PseudoCode").
async search(withDetails = false) {
const query = {
withDetails
}
const data: MyApiResponse = await axios.post(uri, query)
}
That’s what I want to achieve.
async search(withDetails = false) {
const query = {
withDetails
}
const data: withDetails ? ResponseWithDetails : ResponseWithoutDetails = await axios.post(uri, query)
}
Solution
Since the code may see either type at runtime, you have to allow for both. Usually you’d use a union type, and then when you need to know whether there are details, you’d look at some aspect of the response that tells you whether there are details (for instance, check the presence of a property that only exists on ResponseWithDetails
; I’ve used details
in the example):
async search(withDetails = false) {
const query = {
withDetails
};
const data: ResponseWithDetails | ResponseWithoutDetails = (await axios.post(uri, query)).data;
// ...code that uses the common parts of that type...
// If code needs to know there are details:
if ("details" in data) {
// Here, TypeScript knows `data` is `ResponseWithDetails`
} else {
// Here, TypeScript knows `data` is `ResponseWithoutDetails`
}
}
(Note the slight unrelated modification to the code: axios
wraps the response in an AxiosResponse
object. The actual data is available as the data
property, so I added using that data property.)
This avoids using any type assertions (as whatever
), which is generally best when you can avoid it because it’s easy to make a false type assertion and find your types don’t accurately reflect the runtime values they’re supposed to reflect.
In comments you’ve said that you don’t actually use data
in the function, you just want to return it to the caller. You can do that with a function overload, like this:
async search(): Promise<ResponseWithoutDetails>;
async search(withDetails: false): Promise<ResponseWithoutDetails>;
async search(withDetails: true): Promise<ResponseWithDetails>;
async search(withDetails: boolean): Promise<ResponseWithoutDetails | ResponseWithDetails>; // See note on this one
async search(withDetails = false): Promise<ResponseWithoutDetails | ResponseWithDetails> {
const query = {
withDetails
};
const { data } = await axios.post(uri, query);
return data;
}
(You only need the declaration with See note on this one
on it if you want to allow a runtime boolean value when calling search
; if you want to require a compile-time constant, just remove that one.)
How that works is that when the argument is provided as a compile-time constant (like a literal true
or false
), TypeScript will know that the type is ResponseWithDetails
or ResponseWithoutDetails
respectively. It’ll also know that a call with no argument will return ResponseWithoutDetails
. It’s only if you provide it with a runtime boolean
value as an argument that the caller will see ResponseWithoutDetails | ResponseWithDetails
.
Note that in the paragraph above I left off the Promise<...>
for clarity, but of course the real types are Promise<ResponseWithoutData>
and Promise<ResponseWithData>
. But when you await search
, the result is the fulfillment value of hte promise (ResponseWithoutData
or ResponseWithData
).
One final note: I made a point about not using type assertions when you can avoid it, and I stand by that point, but it’s important to remember that when you’re at the interface between TypeScript and the untyped world (the data you get back from axios), you’re always doing a type assertion in some sense. (We could move it into a type argument on the axios.post
call, but that’s basically still there.) But if you do them in nice contained places like your search
, you’re safely back in typed land from there on. 🙂
Answered By – T.J. Crowder
Answer Checked By – Robin (BugsFixing Admin)