Narrowing a union type
In typescript, a union type describes a value that can be one of several types separated by the vertical
| bar, for example:
1 2 3 4 5
In the above code
text can either be a string or an array of strings.
Problems arise in union types because you can only access members that are common to all types in the union.
I have recently been commiting code to afterjs to add better type safety to the existing code.
On one of my PRs I created the following union to model all the component types that afterjs might have to deal with when deciding whether or not the component is loaded via a dynamic import. The compponent might be an
AyncComponent that knows how to load itself via a dynamic import or it might be a react-router aware component or just a plain old react component. I created the following union:
1 2 3 4
AsyncRouteableComonent will have the following interface that gets added in a previous type.
1 2 3 4
There is also in typescript the concept of
Type Guards. A type guard is an expression that performs a runtime check that guarantees that you have the requested type. I came up with this type guard to ensure I am dealing with an
isAsyncComponent is called, typescript will narrow the specific variable to that specific type:
1 2 3
There is a gotcha though and I encountered it with this code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
Typescript narrows the scope in all cases due to the
isAsyncComponent type guard on line 4 apart from in the anonymous function resolve handler of the
load promise on line 7:
route.component in the anonomus function that resolves after
load gives the following compiler error:
Property ‘getInitialProps’ does not exist on type ‘AsyncRouteableComponent
’. Property ‘getInitialProps’ does not exist on type ‘ComponentClass<RouteComponentProps<any, StaticContext>>’.
Typescript has not narrowed the scope because it cannot find a
getInitialProps member on all types of the union.
It turns out narrowing for a mutable variable such as
route.component does not apply inside callbacks such as the anonymous function resolve handler because typescript does not trust that the local variable will not be reassigned before the callback executes. There is a whole big thread about it here.
The workaround is to copy
route.component to a local variable before the route guard is called.
Here is the working solution:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
The copy happens on line 11 with
const used to ensure the local variable cannot be reassigned, typescript is satisfied that it cannot be reassigned and the scope is narrowed for the union and the correct scope is applied.