Way to segment discriminated union into types with and without property

293 Views Asked by At

Lets say I have a discriminated union like so:

type Route = HomeRoute | ProfileRoute | BlogRoute;


type HomeRoute = {
  route: '/home'
}

type ProfileRoute = {
  route: '/profile/:userId',
  params: {
    userId: string;
  }
}

type BlogRoute = {
  route: '/blog/:teamId',
  params: {
    teamId: string;
  }
}

And I have a function which operates on Route objects, with some optional logic if they have params:

function processRoute(route: Route) {
  if ('params' in route) {
    const { params } = route; // <-- this errors
  }
}

There doesn't seem to be a way (that I can see) to check for params without adding an any annotation...

function paramsInRoute(route: any): route is { params: {[key: string]: string} } {
  return ('params' in route);
}

function processRoute(route: Route) {
  if ('params' in route) {
    const { params } = route; // <-- this errors
  }

  if (paramsInRoute(route)) {
    const { params } = route; // <-- this typechecks
  }
}

Is there a way to do the above without casting to any (in the parameter of paramsInRoute)?

playground link

1

There are 1 best solutions below

2
On BEST ANSWER

I, personally, would be happy using the type guard you have:

function paramsInRoute(route: any): route is { params: {[key: string]: string} } {
  return ('params' in route);
}

since TypeScript certainly narrows a passed-in Route object to ProfileRoute | BlogRoute

if (paramsInRoute(route)) {
    const notHomeRouteAnymore: ProfileRoute | BlogRoute = route; // no error
}

But if you're worried about someone doing this:

const notRoute = "This is not a route";
if (paramsInRoute(notRoute)) { notRoute.params.oops = 'what' };

then you can do this:

function paramsInRoute(route: Route): route is Route & { params: {[key: string]: string} } {
  return ('params' in route);
}

which does the same narrowing but prevents the erroneous call:

paramsInRoute(notRoute); // error

Hope that helps; good luck!