fenok / react-router-typesafe-routes

Comprehensive and extensible type-safe routes for React Router v6 with first-class support for nested routes and param validation.
MIT License
145 stars 3 forks source link

Proper way to access routes types from vanilla TS code? #52

Open dreyescabrera opened 4 months ago

dreyescabrera commented 4 months ago

Is there a direct way to extract the type of route data (e.g. search params) from the ROUTES object? I couldn't find it after reading the documentation.

However, I did find a workaround:

type Step = ReturnType<typeof ROUTES.SIGN_UP.getTypedSearchParams>['step'] // "account" | "additional-info" | "verify" 

It's just a matter of preference, I'd like to work this way. I could also decouple the route typing from the ROUTES object, like

const steps = ['account', 'additional-info', 'verify'] as const

export type Step = steps[number]

export const ROUTES = {
  SIGN_UP: route('sign-up', {
    searchParams: {
      step: union(steps),
    },
  }),
}
fenok commented 3 months ago

It's a pretty complex question.

First of all, there are three kinds of types here:

Getting original types

The intended way is to decouple the typing, as you did in the second example:

const steps = ["account", "additional-info", "verify"] as const;

export type Step = (typeof steps)[number];

export const ROUTES = {
    SIGN_UP: route("sign-up", {
        searchParams: {
            step: union(steps),
        },
    }),
};

Specifically for string unions, I find enums more suitable here. I know they're frowned upon, but the only downside when used like this is that the type is effectively branded (which may actually be desired).

export enum Step {
    ACCOUNT = "account",
    ADDITIONAL_INFO = "additional-info",
    VERIFY = "verify",
}

export const ROUTES = {
    SIGN_UP: route("sign-up", {
        searchParams: {
            step: union(Object.values(Step)),
        },
    }),
};

As for extracting these types from the route object, well... it's quite tricky and requires a somewhat complex custom helper. I don't have time to write a comprehensive one right now, but here is a draft that handles search params:

type Test = ExtractOriginalTypes<typeof ROUTES.SIGN_UP, "search">["step"];

type ExtractOriginalTypes<TRoute, TKind extends "search", TMode extends "out" | "in" = "out"> = TRoute extends Route<
    infer TPath,
    infer TPathParams,
    infer TSearchParams,
    infer THash,
    infer TState
>
    ? ExtractOriginalSearchTypes<TSearchParams, TMode>
    : never;

type ExtractOriginalSearchTypes<TSearchParams, TMode extends "out" | "in" = "out"> = {
    [TKey in keyof TSearchParams]: ExtractSearchType<TSearchParams[TKey], TMode>;
};

type ExtractSearchType<TType, TMode extends "out" | "in"> = TType extends SearchParamType<infer TOut, infer TIn>
    ? TMode extends "out"
        ? TOut
        : TIn
    : never;

Getting input / output params types

In your workaround, you get an output param type (which actually should include undefined, if that's not the case, please verify that you have "strict": true in your tsconfig, as the library may not work as intended without it). It's fairly easy to write generic helpers for extracting input and output params types:

type ExtractOutParams<TRoute, TKind extends "pathname" | "search" | "hash" | "state"> = TRoute extends Route<
    infer TPath,
    infer TPathTypes,
    infer TSearchTypes,
    infer THash,
    infer TState
>
    ? TKind extends "pathname"
        ? OutParams<TPath, TPathTypes>
        : TKind extends "search"
        ? OutSearchParams<TSearchTypes>
        : TKind extends "hash"
        ? THash[number] | undefined
        : OutStateParams<TState>
    : never;

type ExtractInParams<TRoute, TKind extends "pathname" | "search" | "hash" | "state"> = TRoute extends Route<
    infer TPath,
    infer TPathTypes,
    infer TSearchTypes,
    infer THash,
    infer TState
>
    ? TKind extends "pathname"
        ? InParams<TPath, TPathTypes>
        : TKind extends "search"
        ? InSearchParams<TSearchTypes>
        : TKind extends "hash"
        ? THash[number] | undefined
        : InStateParams<TState>
    : never;
fenok commented 3 months ago

I realised that it's not immediately obvious: the only difference between original types and input/output route params types is that undefined is added or removed as necessary, which may or may not be important in your specific case.