33345-extreme-dynamic-route

Back

// 参数类型枚举类型
enum ParamsType {
  STATIC,
  SINGLE,
  REST,
  OPTIONAL_REST,
}

// 获取所有路由参数
type GetParams<
  T extends string,
  Res extends string[] = []
> = T extends `${infer Left}/${infer Right}`
  ? GetParams<Right, Left extends "" ? Res : [...Res, Left]>
  : T extends ""
  ? Res
  : [...Res, T];

// 解析路由参数类型
type ParseParams<
  T extends string[],
  Res extends [string, ParamsType][] = []
> = T extends [infer First extends string, ...infer Rest extends string[]]
  ? First extends `[[...${infer P}]]`
    ? ParseParams<Rest, [...Res, [P, ParamsType.OPTIONAL_REST]]>
    : First extends `[...${infer P}]`
    ? P extends ""
      ? ParseParams<Rest, [...Res, ["...", ParamsType.SINGLE]]>
      : ParseParams<Rest, [...Res, [P, ParamsType.REST]]>
    : First extends `[${infer P}]`
    ? P extends ""
      ? ParseParams<Rest, Res>
      : ParseParams<Rest, [...Res, [P, ParamsType.SINGLE]]>
    : ParseParams<Rest, [...Res, [First, ParamsType.STATIC]]>
  : Res;

// 检查路由参数序列是否合法
type CheckValidity<
  T extends [string, ParamsType][],
  Flag extends boolean = true
> = T extends [
  infer First extends [string, ParamsType],
  ...infer Rest extends [string, ParamsType][]
]
  ? First[1] extends ParamsType.STATIC
    ? CheckValidity<Rest, true>
    : First[1] extends ParamsType.SINGLE
    ? CheckValidity<Rest, Flag>
    : Flag extends true
    ? CheckValidity<Rest, false>
    : false
  : true;

// 展开对象的交叉类型
type ObjectFlatten<T extends object> = {
  [K in keyof T]: T[K];
}; 

// 主类型
type DynamicRoute<
  T extends string,
  P extends [string, ParamsType][] = ParseParams<GetParams<T>>
> = CheckValidity<P> extends true
  ? ObjectFlatten<
      {
        [K in P[number] as K[1] extends ParamsType.SINGLE | ParamsType.REST
          ? K[0]
          : never]: K[1] extends ParamsType.SINGLE ? string : string[];
      } & {
        [K in P[number] as K[1] extends ParamsType.OPTIONAL_REST
          ? K[0]
          : never]?: string[];
      }
    >
  : never;

大致思如下:

  1. 在动态路由中参数分四类,为其编写枚举类型。

  2. 以斜杠为分隔符,获取所有路由参数,以数组形式保存。

  3. 解析每个参数的类型,将每个参数及其类型组成一个新数据结构,以数组形式保存。

  4. 检查动态路由参数序列能否进行正确匹配,规则为如下几条:

    • 静态参数后可跟任意参数
    • 不能有连续的数组/可选数组参数
    • 单个动态参数不能在数组/可选数组中间
  5. 若参数数组通过检查,则根据参数类型生成结果对象,否则返回 never

Solution by Barrenboat #37431

// Tokens
type StaticSegment = "StaticSegment"; // app
type SingleSegment = "SingleSegment"; // [slug], [...]
type OptSingleSegment = "OptSingleSegment"; // [[slug]], [[...]]
type MultiSegment = "MultiSegment"; // [...slug]
type OptMultiSegment = "OptMultiSegment"; // [[...slug]]
type KeyFromSlug<TSlug> = TSlug extends "" ? never : TSlug;

// Tokenizers
type TokenizeSegment<TStr extends string> =
	// special cases
	TStr extends `[[...]]` ? { token: OptSingleSegment; data: { "..."?: string } }
	: TStr extends `[...]` ? { token: SingleSegment; data: { "...": string } }
	: // begin of sensible cases
	TStr extends `[[...${infer Slug}]]` ?
		{ token: OptMultiSegment; data: { [K in KeyFromSlug<Slug>]?: string[] } }
	: TStr extends `[...${infer Slug}]` ?
		{ token: MultiSegment; data: { [K in KeyFromSlug<Slug>]: string[] } }
	: TStr extends `[[${infer Slug}]]` ?
		{ token: OptSingleSegment; data: { [K in KeyFromSlug<Slug>]?: string } }
	: TStr extends `[${infer Slug}]` ?
		{ token: SingleSegment; data: { [K in KeyFromSlug<Slug>]: string } }
	:	{ token: StaticSegment; data: {} };

type TokenizedSegment = TokenizeSegment<any>;

type TokenizePath<TPath extends string, TCollector extends any[] = []> =
	TPath extends `${infer Segment}/${infer RemainingPath}` ?
		TokenizePath<RemainingPath, [...TCollector, TokenizeSegment<Segment>]>
	:	[...TCollector, TokenizeSegment<TPath>];

type TokenizeUrl<TUrl extends string> =
	TUrl extends `/${infer PathSegment}` ? TokenizePath<PathSegment> : TokenizePath<TUrl>;

// Parser
type Parser$Start<TSegments extends TokenizedSegment[]> =
	TSegments extends (
		[
			infer TSegment extends TokenizedSegment,
			...infer TRemainingSegments extends TokenizedSegment[],
		]
	) ?
		TSegment["token"] extends StaticSegment ?
			Parser$Start<TRemainingSegments> & TSegment["data"]
		: TSegment["token"] extends SingleSegment ?
			Parser$Start<TRemainingSegments> & TSegment["data"]
		: TSegment["token"] extends OptSingleSegment ?
			Parser$Start<TRemainingSegments> & TSegment["data"]
		: TSegment["token"] extends MultiSegment ?
			Parser$MultiSegmentRead<TRemainingSegments> & TSegment["data"]
		: TSegment["token"] extends OptMultiSegment ?
			Parser$MultiSegmentRead<TRemainingSegments> & TSegment["data"]
		:	never // Error (should never happen)
	:	{}; // TSegments is empty

type Parser$MultiSegmentRead<TSegments extends TokenizedSegment[]> =
	TSegments extends (
		[
			infer TSegment extends TokenizedSegment,
			...infer TRemainingSegments extends TokenizedSegment[],
		]
	) ?
		TSegment["token"] extends StaticSegment ?
			Parser$Start<TRemainingSegments> & TSegment["data"]
		: TSegment["token"] extends SingleSegment ?
			Parser$MultiSegmentRead<TRemainingSegments> & TSegment["data"]
		: TSegment["token"] extends OptSingleSegment ?
			Parser$MultiSegmentRead<TRemainingSegments> & TSegment["data"]
		:	never // Error
	:	{}; // TSegments is empty

/**
 * mermaid state diagramm for the Parser:
stateDiagram-v2
    [*] --> start
    start --> start :StaticSegment, SingleSegment OptSingleSegment
    start --> multiSegmentRead :MultiSegment OptMultiSegment

    multiSegmentRead --> start :StaticSegment
    multiSegmentRead --> multiSegmentRead :SingleSegment OptSingleSegment
    multiSegmentRead --> error :MultiSegment OptMultiSegment
 */
type Parser<T extends string> = Parser$Start<TokenizeUrl<T>>;

type JoinIntersections<T> = {
	[K in keyof T]: T[K];
};
type DynamicRoute<T extends string> = JoinIntersections<Parser<T>>;

Solution by MartinElsaesser #37392

method: slice string to tuple --> check every element and maintain a boolean flag to return never when necessary.

type Slice<T,R extends string[] = []> = 
  T extends `/${infer F extends string}/${infer Rest extends string}` 
    ? Slice<`${Rest}`,[...R,F]>
    :T extends  `${infer F extends string}/${infer Rest extends string}`
      ?  Slice<`${Rest}`,[...R,F]>
      :[...R,T];


type Check<T extends string[],R  = {},Ambiguous extends boolean = false> = 
  T extends [infer F,...infer Rest extends string[]] 
    ? F extends `[[...${infer Key extends string}]]`
      ? Ambiguous extends true 
        ? never 
        : Check<Rest,R & {[K in Key]?:string[]},true>
      :F extends `[...]${string}`
        ? Check<Rest,R & {'...':string} ,false>
        :F extends `[...${infer Key extends string}]` 
          ? Ambiguous extends true 
            ? never
            :Check<Rest,R & {[K in Key]:string[]},true>
          :F extends  `[]`
            ? Check<Rest,R ,Ambiguous>
            :F extends  `[${infer Key extends string}]` 
              ? Check<Rest,R & {[K in Key]:string},Ambiguous>
              :Check<Rest,R ,false>
    :R;

type Normalize<T> = [T] extends [never] ? never : Omit<T,never>;
type DynamicRoute<T extends string> = Normalize<Check<Slice<T>>>;

Solution by sciencefunq #37379

type ParseContent<Prefix, Key, P> = `${Prefix & string}${Key & string}` extends '' 
? null 
: Key extends '' 
? { key: '...', type: string, partial: P } 
: { key: Key, type: Prefix extends '...' ? string[] : string, partial: P }

type ParseSegment<T> = T extends `${string}[[...${infer Key}]]${string}`
? ParseContent<'...', Key, true>
: T extends `${string}[[${infer Key}]]${string}`
? ParseContent<'', Key, true>
: T extends `${string}[...${infer Key}]${string}`
? ParseContent<'...', Key, false>
: T extends `${string}[${infer Key}]${string}`
? ParseContent<'', Key, false>
: null;

type AddSeg<Base, Key extends string, Type, P extends boolean = false> = Omit<Base & (P extends true ? {[K in Key]?: Type} : {[K in Key]: Type}), never>

type Route<T, R = {}, PrevArray extends boolean = false> = T extends `${infer Head}/${infer Tail}`
? ParseSegment<Head> extends {key: infer Key extends string, type: infer Type, partial: infer P extends boolean}
  ? `${Type extends any[] ? true : false}${PrevArray}` extends 'truetrue' 
    ? never
    : Route<Tail, AddSeg<R, Key, Type, P>, Type extends any[] ? true : PrevArray>
  : Route<Tail, R, false>
: R;

type DynamicRoute<T> = T extends `${string}/` ? Route<T> : Route<`${T & string}/`>

Solution by user65536 #36482

type Merge<T> = {
  [P in keyof T]: T[P];
};

type DynamicRoute<
  T extends string,
  Check extends boolean = false,
  Result = {}
> = T extends `${"/" | ""}[[...${infer P}]]${infer Rest}`
  ? Check extends true
    ? never
    : DynamicRoute<Rest, true, Merge<Result & { [K in P]?: string[] }>>
  : T extends `${"/" | ""}[...${infer P}]${infer Rest}`
  ? P extends ""
    ? DynamicRoute<Rest, Check, Merge<Result & { "...": string }>>
    : Check extends true
    ? never
    : DynamicRoute<
        Rest,
        true,
        Merge<Result & { [K in `${P}`]: P extends "" ? string : string[] }>
      >
  : T extends `${"/" | ""}[${infer P1}${infer P2}]${infer Rest}`
  ? DynamicRoute<Rest, Check, Merge<Result & { [K in `${P1}${P2}`]: string }>>
  : T extends `${"/" | ""}${infer _P1}${infer _P2}/${infer Rest}`
  ? DynamicRoute<Rest, false, Result>
  : Result;

Solution by yukicountry #34556

type SlashSplit<T extends string> =
  T extends `` ? [] :
  T extends `/${infer TSingle}` ? [...SlashSplit<TSingle>] :
  T extends `${infer TDouble1}/${infer TDouble2}` ? [TDouble1, ...SlashSplit<TDouble2>] :
  [T];

type PartialRoute<S extends string> = S extends `[[...]]` ? { [K in `...`]?: string } :
                                      S extends `[...]` ? { [K in `...`]: string } :
                                      S extends `[[...${infer OptionalArrayName}]]` ? { [K in OptionalArrayName]?: string[] } :
                                      S extends `[[${infer OptionalStringName}]]` ? { [K in OptionalStringName]?: string } :
                                      S extends `[...${infer ArrayName}]` ? { [K in ArrayName]: string[] } :
                                      S extends `[${infer StringName}]` ? { [K in StringName as StringName extends "" ? never : K]: string } :
                                      {}

type ObjectValue<T> = T[keyof T];

type PathObject<S extends string[], Obj = {}, AddedArrayAndNoString = false> =
  S extends [infer FirstElem extends string, ...infer Rest extends string[]] ?
    string[] extends ObjectValue<PartialRoute<FirstElem>> ?
      // if we have added an array without a string in between then it is invalid
      AddedArrayAndNoString extends true ?
        never :
        PathObject<Rest, PartialRoute<FirstElem> & Obj, true> :
      // no dynamic params case
      ObjectValue<PartialRoute<FirstElem>> extends never ?
        PathObject<Rest, PartialRoute<FirstElem> & Obj, false> :
        PathObject<Rest, PartialRoute<FirstElem> & Obj, AddedArrayAndNoString> :
    Obj

// this type is necessary because apparently { foo: string } & { bar: string }
// is not equivalent to { foo: string, bar: string }, learnt something new
type Mix<Obj> = {
  [K in keyof Obj]: Obj[K]
}

type DynamicRoute<T extends string> = Mix<PathObject<SlashSplit<T>>>;

This is my first of these puzzles, any feedback would be much appreciated!

Solution by M-Kusumgar #33631

type IsArr<T> = T extends `${string}...${string}` ? true : false
type Extract<T> = T extends `[...${infer F}]` | `...${infer F}` ? F : T

type DynamicRoute<T extends string, _Res extends Record<string, string | string[]> = {}> = T extends `${infer A}[...]${infer B}`
  ? DynamicRoute<`${A}${B}`, _Res & { '...': string }>
  : T extends `${string}]/[...${string}`
    ? never
      : T extends `${infer A}[[${ infer F }]]${ infer B }`
      ? DynamicRoute<`${A}${B}`, _Res & ( Extract<F> extends '' ? {} : { [K in Extract<F>]?: IsArr<F> extends true ? string[] : string } ) >
      : T extends `${infer A}[${ infer F }]${ infer B }`
        ? DynamicRoute<`${A}${B}`, _Res & ( Extract<F> extends '' ? {} : { [K in Extract<F>]: IsArr<F> extends true ? string[] : string } ) >
    : { [K in keyof _Res]: _Res[K] }

Solution by lvjiaxuan #33614

// `Guard` prevents consecutive [[...OptCatchAll]] or [...CatchAll] segments;
// returns never if found, even with [Param] in between.
type DynamicRoute<S extends string, R = {}, Guard extends {} = {}>
  = S extends `${infer Segment}/${infer Rest}`
    ? Segment extends `[[...${infer OptCatchAll extends `${any}${any}`}]]`
      ? DynamicRoute<Rest, R & Partial<Record<OptCatchAll, string[]>>, never> & Guard
    : Segment extends `[...${infer CatchAll extends `${any}${any}`}]`
      ? DynamicRoute<Rest, R & Record<CatchAll, string[]>, never> & Guard
    : Segment extends `[${infer Param extends `${any}${any}`}]`
      ? DynamicRoute<Rest, R & Record<Param, string>, Guard>
    // If not [Param], reset Guard (using & {} has no impact, but & never will cause return to never)
    : DynamicRoute<Rest, R, {}>
  : S extends '' ? {[P in keyof R]: R[P]} : DynamicRoute<`${S}/`, R, Guard>

Playground

type DynamicRoute<S extends string, R = {}>
  = S extends `${infer Segment}/${infer Rest}`
    ? Segment extends `[[...${infer OptCatchAll extends `${any}${any}`}]]` ? StaticRoute<Rest, R & Partial<Record<OptCatchAll, string[]>>>
    : Segment extends `[...${infer CatchAll extends `${any}${any}`}]` ? StaticRoute<Rest, R & Record<CatchAll, string[]>>
    : Segment extends `[${infer Param extends `${any}${any}`}]` ? DynamicRoute<Rest, R & Record<Param, string>>
    : DynamicRoute<Rest, R>
  : S extends '' ? {[P in keyof R]: R[P]} : DynamicRoute<`${S}/`, R>

type StaticRoute<S extends string, R = {}>
  = S extends `${infer Segment}/${infer Rest}`
    ? Segment extends `[[...${any}${any}]]` | `[...${any}${any}]` ? never
    : Segment extends `[${infer Param extends `${any}${any}`}]` ? StaticRoute<Rest, Omit<R & Record<Param, string>, never>>
    : DynamicRoute<Rest, R>
  : S extends '' ? {[P in keyof R]: R[P]} : StaticRoute<`${S}/`, R>

Playground

Solution by teamchong #33350