core-api: add SubRouteRef type

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2021-03-06 23:52:02 +01:00
parent 5ab5864f6b
commit 87973f5913
2 changed files with 96 additions and 3 deletions
+82 -2
View File
@@ -16,6 +16,7 @@
import {
RouteRef,
SubRouteRef,
ExternalRouteRef,
routeRefType,
AnyParams,
@@ -85,12 +86,91 @@ export function createRouteRef<
/** @deprecated Route refs no longer decide their own title */
title: string;
}): RouteRef<OptionalParams<Params>> {
return new RouteRefImpl<OptionalParams<Params>>({
return new RouteRefImpl({
...config,
params: (config.params ?? []) as ParamKeys<OptionalParams<Params>>,
});
}
export class SubRouteRefImpl<Params extends AnyParams>
extends RouteRefBase
implements SubRouteRef<Params> {
readonly [routeRefType] = 'sub';
constructor(
id: string,
readonly path: string,
readonly parent: RouteRef,
readonly params: ParamKeys<Params>,
) {
super('sub', id);
}
}
// These utility types help us infer a Param object type from a string path
// For example, `/foo/:bar/:baz` inferred to `{ bar: string, baz: string }`
type ParamPart<S extends string> = S extends `:${infer Param}` ? Param : never;
type ParamNames<S extends string> = S extends `${infer Part}/${infer Rest}`
? ParamPart<Part> | ParamNames<Rest>
: ParamPart<S>;
type PathParams<S extends string> = { [name in ParamNames<S>]: string };
/**
* Merges a param object type with with an optional params type into a params object
*/
type MergeParams<
P1 extends { [param in string]: string },
P2 extends AnyParams
> = (P1[keyof P1] extends never ? {} : P1) & (P2 extends undefined ? {} : P2);
/**
* Creates a SubRouteRef type given the desired parameters and parent route parameters.
* The parameters types are merged together while ensuring that there is no overlap between the two.
*/
type MakeSubRouteRef<
Params extends { [param in string]: string },
ParentParams extends AnyParams
> = keyof Params & keyof ParentParams extends never
? SubRouteRef<OptionalParams<MergeParams<Params, ParentParams>>>
: never;
export function createSubRouteRef<
Path extends string,
ParentParams extends AnyParams = never
>(config: {
id: string;
path: Path;
parent: RouteRef<ParentParams>;
}): MakeSubRouteRef<PathParams<Path>, ParentParams> {
const { id, path, parent } = config;
type Params = PathParams<Path>;
// Collect runtime parameters from the path, e.g. ['bar', 'baz'] from '/foo/:bar/:baz'
const pathParams = path.split(/:([^/]+)/).filter((_, i) => i % 2 === 1);
const params = [...parent.params, ...pathParams];
if (parent.params.some(p => pathParams.includes(p as string))) {
throw new Error(
'SubRouteRef may not have params that overlap with its parent params',
);
}
if (!path.startsWith('/')) {
throw new Error(`SubRouteRef path sub starts with '/', got '${path}'`);
}
// We ensure that the type of the return type is sane here
const subRouteRef = new SubRouteRefImpl(
id,
path,
parent,
params as ParamKeys<MergeParams<Params, ParentParams>>,
) as SubRouteRef<OptionalParams<MergeParams<Params, ParentParams>>>;
// But skip type checking of the return value itself, because the conditional
// type checking of the parent parameter overlap is tricky to express.
return subRouteRef as any;
}
export class ExternalRouteRefImpl<
Params extends AnyParams,
Optional extends boolean
@@ -131,7 +211,7 @@ export function createExternalRouteRef<
*/
optional?: Optional;
}): ExternalRouteRef<OptionalParams<Params>, Optional> {
return new ExternalRouteRefImpl<OptionalParams<Params>, Optional>(
return new ExternalRouteRefImpl(
options.id,
(options.params ?? []) as ParamKeys<OptionalParams<Params>>,
Boolean(options.optional) as Optional,
+14 -1
View File
@@ -41,6 +41,16 @@ export type RouteRef<Params extends AnyParams = any> = {
title?: string;
};
export type SubRouteRef<Params extends AnyParams = any> = {
readonly [routeRefType]: 'sub';
parent: RouteRef;
path: string;
params: ParamKeys<Params>;
};
export type ExternalRouteRef<
Params extends AnyParams = any,
Optional extends boolean = any
@@ -52,7 +62,10 @@ export type ExternalRouteRef<
optional?: Optional;
};
export type AnyRouteRef = RouteRef<any> | ExternalRouteRef<any, any>;
export type AnyRouteRef =
| RouteRef<any>
| SubRouteRef<any>
| ExternalRouteRef<any, any>;
// TODO(Rugvip): None of these should be found in the wild anymore, remove in next minor release
/** @deprecated */