Veksi
Veksi

Reputation: 3758

How to convert TypeScript discriminated union into an object

Say, I have a structure like

//Modified from https://blog.parametricstudios.com/posts/pattern-matching-custom-data-types/.

enum RouteEnum 
{
  Home = "/",
  Todos = "/todos",
  Todo = "/todos/:id",
  NotFound = "*"
}

class HomeLocation
{
  readonly route = RouteEnum.Home;
  
  match<Out>(matcher: LocationMatcher<Out>): Out
  {
    return matcher[RouteEnum.Home](this);
  }
}

// A list of all Todo item links, when one is linked the next route is in TodoLocation.
class TodosLocation
{
  readonly route = RouteEnum.Todos;

  match<Out>(matcher: LocationMatcher<Out>): Out
  {
    return matcher[RouteEnum.Todos](this);
  }
}

// A single Todo location.
class TodoLocation 
{
  readonly route = RouteEnum.Todo;

  match<Out>(matcher: LocationMatcher<Out>): Out
  {
    return matcher[RouteEnum.Todo](this);
  }
}


class NotFoundLocation 
{
  readonly route = RouteEnum.NotFound;

  match<Out>(matcher: LocationMatcher<Out>): Out
  {
    return matcher[RouteEnum.NotFound](this);
  }
}

type NewLocation = HomeLocation | TodosLocation | TodoLocation | NotFoundLocation;

type LocationMatcher<Out> =
{
  [RouteEnum.Home]: (route: HomeLocation) => Out;
  [RouteEnum.Todos]: (route: TodosLocation) => Out;
  [RouteEnum.Todo]: (route: TodoLocation) => Out;
  [RouteEnum.NotFound]: (route: NotFoundLocation) => Out;
};

would it be possible to turn NewLocation into a structure like

// The matching order here is from the the first to latter.
const routes = {
  '/': HomeLocation,
  '/todos': TodosLocation,
  '/todos/:id': TodoLocation,
  '/*': NotFoundLocation
}

In case it matters, the larger reason for this question is that I'm thinking to do route matching over this structure but naturally there needs to be a way to turn URLs to those *Location classes.

One example could be using something like https://github.com/CaptainCodeman/js-router (or a similar approach that more readily fits this structure). This particular library for instance operates on a structure like that routes example. I think that even without using this specific library knowing how to enumerate over the discriminated union and create a new type would be good to know information.

The goal would be to have the parsed parameters as strongly typed. This is manual, of course, but they need to matched first from their corresponding route in the *Location classes, in one way or another. One option is to have a callback function per *Location class that takes the input from a matched route.

Edit I wrote a StackBlitz to illustrate my pondering better. It's at https://stackblitz.com/edit/typescript-hl6drb?file=index.ts.

Upvotes: 1

Views: 367

Answers (1)

jcalz
jcalz

Reputation: 327944

If you want to programmatically generate an object type where the keys come from the route property of the NewLocation union members and where the values are constructors for the corresponding element of NewLocation, then you can do it like this:

type RoutesType = { 
  [K in NewLocation['route']]: new () => Extract<NewLocation, { route: K }> 
};

That evaluates to the equivalent of

type RoutesType = {
    "/": new () => HomeLocation;
    "/todos": new () => TodosLocation;
    "/todos/:id": new () => TodoLocation;
    "*": new () => NotFoundLocation;
}

and you can then annotate the routes variable as that type:

const routes: RoutesType = {
  '/': HomeLocation,
  '/todos': TodosLocation,
  '/todos/:id': TodoLocation,
  '*': NotFoundLocation
}

note that this is the same thing as

const routes: RoutesType = {
  [RouteEnum.Home]: HomeLocation,
  [RouteEnum.Todos]: TodosLocation,
  [RouteEnum.Todo]: TodoLocation,
  [RouteEnum.NotFound]: NotFoundLocation
}

so you can write that either way if you want.

Does that work for you? Hope that helps; good luck!

Playground link


There's also something like this which is the closest I can get to going from values to types here...

Upvotes: 2

Related Questions