From 87973f5913ba282c9de97b6f2b1b168a10acbbbb Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 6 Mar 2021 23:52:02 +0100 Subject: [PATCH] core-api: add SubRouteRef type Signed-off-by: Patrik Oldsberg --- packages/core-api/src/routing/RouteRef.ts | 84 ++++++++++++++++++++++- packages/core-api/src/routing/types.ts | 15 +++- 2 files changed, 96 insertions(+), 3 deletions(-) diff --git a/packages/core-api/src/routing/RouteRef.ts b/packages/core-api/src/routing/RouteRef.ts index 24f93eeeeb..4a28301173 100644 --- a/packages/core-api/src/routing/RouteRef.ts +++ b/packages/core-api/src/routing/RouteRef.ts @@ -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> { - return new RouteRefImpl>({ + return new RouteRefImpl({ ...config, params: (config.params ?? []) as ParamKeys>, }); } +export class SubRouteRefImpl + extends RouteRefBase + implements SubRouteRef { + readonly [routeRefType] = 'sub'; + + constructor( + id: string, + readonly path: string, + readonly parent: RouteRef, + readonly params: ParamKeys, + ) { + 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 `:${infer Param}` ? Param : never; +type ParamNames = S extends `${infer Part}/${infer Rest}` + ? ParamPart | ParamNames + : ParamPart; +type PathParams = { [name in ParamNames]: 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>> + : never; + +export function createSubRouteRef< + Path extends string, + ParentParams extends AnyParams = never +>(config: { + id: string; + path: Path; + parent: RouteRef; +}): MakeSubRouteRef, ParentParams> { + const { id, path, parent } = config; + type Params = PathParams; + + // 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>, + ) as SubRouteRef>>; + + // 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, Optional> { - return new ExternalRouteRefImpl, Optional>( + return new ExternalRouteRefImpl( options.id, (options.params ?? []) as ParamKeys>, Boolean(options.optional) as Optional, diff --git a/packages/core-api/src/routing/types.ts b/packages/core-api/src/routing/types.ts index 69de5345a5..d91cd77bc5 100644 --- a/packages/core-api/src/routing/types.ts +++ b/packages/core-api/src/routing/types.ts @@ -41,6 +41,16 @@ export type RouteRef = { title?: string; }; +export type SubRouteRef = { + readonly [routeRefType]: 'sub'; + + parent: RouteRef; + + path: string; + + params: ParamKeys; +}; + export type ExternalRouteRef< Params extends AnyParams = any, Optional extends boolean = any @@ -52,7 +62,10 @@ export type ExternalRouteRef< optional?: Optional; }; -export type AnyRouteRef = RouteRef | ExternalRouteRef; +export type AnyRouteRef = + | RouteRef + | SubRouteRef + | ExternalRouteRef; // TODO(Rugvip): None of these should be found in the wild anymore, remove in next minor release /** @deprecated */