Issue
I asked myself the question in the title while dealing with this issue: I have function getResponse
from a library which either returns { a: number }
if it's an Android phone or { b: number }
if it's an iOS phone.
I had this code and the assignment to value
here gives me a type error:
const response = getResponse();
const value = isAndroid ? response.a : response.b;
I solved the issue with a type guard by creating a type predicate isAndroidResponse
const isAndroidResponse = (
response: { a: number } | { b: number }
): response is { a: number } => {
return isAndroid; // 'response' parameter not used!
};
and using it as follows:
const response = getResponse();
let value;
if (isAndroidResponse(response)) {
value = response.a;
} else {
value = response.b;
}
However, I have two issues with this solution which make me doubt whether it really is a "good" solution for my problem:
In the type predicate I'm not using the passed
response
variable.It is a lot of code! I would love to have much less code, by perhaps just do something along these lines (which is not correct TS code):
type ResponseT = isAndroid ? { a: number } : { b: number }; const response = getResponse(); const value = isAndroid ? response.a : response.b;
This brings me to my follow-up question: Is my usage of a type predicate a feasible solution or am I misusing it in this case?
I created this TS playground for the described example.
Solution
Conceptually, you have a boolean value isAndroid
which acts as a discriminant for the union type that comes out of getResponse()
. It means that the return type of getResponse()
is like a discriminated union, but one where the discriminant property is floating disconnected from the return value itself. I'll call this a "nonlocal discriminated union" for want of a standard term.
TypeScript really isn't set up to understand nonlocal discriminated unions, so you will need to work around it in some way.
If you really care about minimizing excess code, the best you can do is to use some type assertions to just tell the compiler what it can't verify by itself:
const response = getResponse();
const value = isAndroid ?
(response as AndroidResponse).a :
(response as IOSResponse).b;
This isn't type safe, (e.g., nothing stops you from changing the test from isAndroid
to !isAndroid
) but neither is the implementation of your isAndroidResponse()
user-defined type guard function, nor is any solution that defines isAndroid
prior to the call to getResponse()
.
If you are only writing getResponse()
once or twice in your code, I would recommend type assertions.
If you are going to write getResponse()
many places in your code, then maybe you do want to refactor so that the unsafe part is encapsulated in a single function, like isAndroidResponse()
.
If you do want to do this, my opinion (and this is just an opinion) is that your isAndroidResponse()
implementation is fine. It might surprise people since it seems weird that you'd be able to do anything without inspecting the response
argument. But if you comment it, that might be okay.
Another approach is to make the nonlocal discriminated union into a true discriminated union, by adding isAndroid
as a discriminant property of the response:
// definitions
type Response =
({ isAndroid: true } & AndroidResponse) |
({ isAndroid: false } & IOSResponse)
const getResponseʹ = () =>
Object.assign(getResponse(), { isAndroid }) as Response
// usage
const response = getResponseʹ();
const value = response.isAndroid ? response.a : response.b;
The unsafe code is confined to the implementation of getResponseʹ()
(since we had to assert the return type), but now you can forget about the original isAndroid
flag and instead leverage the support the language has for discriminated unions.
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.