Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
dreyescabrera opened this issue May 16, 2024 · 2 comments
Open

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

dreyescabrera opened this issue May 16, 2024 · 2 comments

Comments

@dreyescabrera
Copy link

dreyescabrera commented May 16, 2024

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
Copy link
Owner

fenok commented May 16, 2024

It's a pretty complex question.

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

  • Original types that are used to create route params (e.g. "account" | "additional-info" | "verify") in your example.
    • Original types are the same for input and output in built-in types but can be different in custom types. Input and output params types are based on them.
  • Input params types (e.g. all search params are optional, so an | undefined is added to the type)
  • Output params types (e.g. search params can be undefined by default, but this can be altered by the .default() or .defined() modifiers)

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;
  • It doesn't work with legacy types (but can be extended to support them)
  • Supporting pathname params properly can be tricky because they depend on the route pattern as well
  • Built-in types are written in such a way that they always add undefined to original types, so original types are effectively lost. It's actually a mistake that will be fixed in the next major version.

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
Copy link
Owner

fenok commented May 16, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants