dagda1
dagda1

Reputation: 28810

narrowing a union type in typescript

I have the following code:

export async function loadInitialProps(routes: AsyncRouteProps[], pathname: string, ctx: any): Promise<InitialProps> {
  const promises: Promise<any>[] = [];

  const match = routes.find((route: AsyncRouteProps) => {
    const matched = matchPath(pathname, route);

    if (matched && route.component && isAsyncComponent(route.component)) {
      promises.push(
        route.component.load
          ? route.component.load().then(() => route.component.getInitialProps({ matched, ...ctx }))
          : route.component.getInitialProps({ matched, ...ctx })
      );
    }

    return !!matched;
  });

  return {
    match,
    data: (await Promise.all(promises))[0]
  };
}

I thought I could narrow the union type with my isAsyncComponent guarded function:

export function isAsyncComponent(Component: AsyncRouteableComponent): Component is AsyncRouteComponentType<any> {
  return (<AsyncRouteComponentType<any>>Component).load !== undefined;
}

My AsyncRouteableComponent type is a union type:

export type AsyncRouteableComponent<Props = any> =
  | AsyncRouteComponentType<RouteComponentProps<Props>>
  | React.ComponentType<RouteComponentProps<Props>>
  | React.ComponentType<Props>;

But typescript thinks the code in the resolved promise function route.component.load().then(() => route.component.getInitialProps({ matched, ...ctx })) is not the type I expect.

if (matched && route.component && isAsyncComponent(route.component)) {
  promises.push(
    route.component.load
      ? route.component.load().then(() => route.component.getInitialProps({ matched, ...ctx }))
      : route.component.getInitialProps({ matched, ...ctx })
  );
}

I get the error message:

Property 'getInitialProps' does not exist on type
'AsyncRouteableComponent<any>'.   Property 'getInitialProps' does not
exist on type 'ComponentClass<RouteComponentProps<any,
StaticContext>>'.

So it does not appear to have narrowed it down after my guard clause.

I can fix it by doing this but I thought the guard function would mean I did not need to do this:

if (matched && route.component && isAsyncComponent(route.component)) {
  const component = route.component as AsyncRouteComponentType<any>;

  promises.push(
    component.load
      ? component.load().then(() => component.getInitialProps({ matched, ...ctx }))
      : component.getInitialProps({ matched, ...ctx })
  );
}

Upvotes: 1

Views: 636

Answers (1)

Matt McCutchen
Matt McCutchen

Reputation: 30919

Narrowing for a mutable local variable such as route does not apply inside callbacks such as your then callback because TypeScript doesn't trust that the local variable won't be reassigned before the callback executes. See this thread for a little more information. The workaround is to copy route (or route.component) to a const variable before you call the guard function.

Upvotes: 1

Related Questions