Search code examples
reactjstypescriptcasl

Passing inherited generic ability type


I'm trying to implement a universal wrapper for pages passed to Route that is limiting access to the page based on current user permissions. This component means to be universal because it's basically doing the same thing: if a user is allowed to go to this page - the page is shown, if not - the not found view is shown.

Implementation

const RouteCan = ({ action, subject }: RouteCan) => {
  return (
    <>
      <Can I={action} as={subject}>
        <Outlet />
      </Can>
      <Can not I={action} as={subject}>
        <NotFoundView />
      </Can>
    </>
  )

// Or ...

  if (can(action, subject)) return <Outlet />

  return <NotFoundView />
}

usage

<Route
  path="/secure-path"
  element={<RouteCan I="view" a="secure-info" />}
>
  /* inner routes */
</Route>

The thing is none of <Can /> or can can accept as arguments just all possible pair variants as my application Ability type is defined as a union of only possible action-subject pairs:

type Ability =
  | ['manage' | 'view', 'secure-path']
  | ['manage' | 'view' | 'edit', 'public-path']

// This causing an error
type RouteCanProps = {
  I: Ability[0]
  as: Ability[1]
}

Is there any opportunity to create a type that provides only possible tuples of values, not just union of all possible actions and subjects separately?

Link on codesandbox - Everything is working as expected, the problem is only on type of RouteCan props.


Solution

  • We need to use distributive conditional types, which can be achieved with the following type:

    type MappedAbility<T extends Ability = Ability> = T extends T
      ? {
          I: T[0];
          as: T[1];
        }
      : never;
    

    The part that allows us to distribute is T extends T, after that the following code is executed for each member of the union separately.

    Usage:

    // type Result = {
    //     I: "manage" | "view";
    //     as: "secure-path";
    // } | {
    //     I: "manage" | "view" | "edit";
    //     as: "public-path";
    // }
    type Result = MappedAbility;
    

    playground