Open dreyescabrera opened 4 months ago
It's a pretty complex question.
First of all, there are three kinds of types here:
"account" | "additional-info" | "verify"
) in your example.
| undefined
is added to the type)undefined
by default, but this can be altered by the .default()
or .defined()
modifiers)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;
undefined
to original types, so original types are effectively lost. It's actually a mistake that will be fixed in the next major version.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;
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.
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:
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