// 参数类型枚举类型
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;
大致思如下:
在动态路由中参数分四类,为其编写枚举类型。
以斜杠为分隔符,获取所有路由参数,以数组形式保存。
解析每个参数的类型,将每个参数及其类型组成一个新数据结构,以数组形式保存。
检查动态路由参数序列能否进行正确匹配,规则为如下几条:
若参数数组通过检查,则根据参数类型生成结果对象,否则返回 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>
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>
Solution by teamchong #33350