First I'm gonna walk through this code just to clarify what's going on:
type A = {
a: number
}
type B = {
a: string
}
const a1: A | B = {a: 1}
The constant a1
has been declared to be of the type that is a union of two object types, A
and B
.
if (typeof(a1) === 'A') {} // (1) Cannot compare the result of `typeof` to string literal `A` because it is not a valid `typeof` return value
The problem here is that we're confusing value-level with type-level. This is sort of confusing because flow shares its type-level typeof
keyword with javascript's value-level typeof
. These are two different things and don't interoperate. We can consider the type-level flow typeof
syntax to be a compile-time operation and the value-level javascript typeof
syntax to be a runtime operation. The important thing about this distinction is that while flow has some awareness of values at compile-time, javascript has no awareness of types at runtime.
So in this example we're getting a runtime value typeof(a1)
and trying to figure out how to compare it with the compile-time type A
, and use this comparison for type refinement. The problem with this is that type refinement must operate at both compile-time and runtime, but runtime will have no awareness of the type A
. Here we're trying to figure out how to refer to A
because flow does not give us a way to do this:
if (typeof(a1) === 'A') // How can we refer to `A` at runtime? We can't.
The other problem here is that typeof a1
is going to return 'object'
in all cases because this is literally just the regular javascript runtime typeof
keyword, and typeof
returns 'object'
for all object instances.
if (a1 instanceof A) {} // (2) Cannot reference type `A` [1] from a value position
This is basically the same problem, but it's more clear what's going on in this example. Flow is telling us exactly what the problem is, we're trying to reference a type (A
) from a value position, rather than a type position. instanceof
is runtime check, so it can't be used to compare against types.
(a1: A) // (3) Cannot cast `a1` to `A` because string [1] is incompatible with `A` [2]
Here we're attempting a type cast / assertion, which doesn't work basically because a1
has not yet been refined to A
, and it so as far as flow knows it could still be B
, and it would be unsafe to not deal with that case.
if (a1.a) { // (4) Works, but inconclusive?
a1.a = 2 // (5) Cannot assign `2` to `a1.a` because number [1] is incompatible with string [2]
a1.a = '2' // (6) Cannot assign `'2'` to `a1.a` because string [1] is incompatible with number [2]
}
This is actually a valid refinement operation, but it only refines the property a1.a
as existing. For example we can do something like this:
type A = {
member: string,
};
type B = {
};
declare var c: A | B;
if (c.member) {
console.log(c.member); // c.member is of type `mixed`
}
(try)
Here we have refined c.member
to existing, but we don't know anything else about it so its type is mixed
. This means we can't do that much with it. The important thing here, is that the type of c
is still A | B
, refining its property did not refine the containing type at all.
Now let's get to the real question:
Which would be the correct way to refine to one of the types in a union, where the type is not a [union of] primitive [types] (i.e. is a [union of object types])?
If the goal is to actually refine at the top level which object type something is, there is currently only one way to do this in flow: Use a disjoint union. A disjoint union relies on a special flag member of object types that must be of a different literal type for each member of the union. This means that if you use something like string
it won't be recognized as a disjoint union:
type A = {
// We're calling this `type` but it could have any name as long
// as it's the same among each member of the union.
type: 'a', // This must be a literal type.
aMember: string,
};
type B = {
type: 'b', // This must be a different literal type.
bMember: number,
};
declare var c: A | B;
if (c.type === 'a') {
// Within this branch, type of `c` is `A`.
(c.aMember: string);
// $FlowFixMe
(c.bMember: number);
} else {
// Within this branch, type of `c` is `B`.
(c.bMember: number);
// $FlowFixMe
(c.aMember: string);
}
(try)