Skip to content

Commit

Permalink
feat: improve type-level performance of Path.ParamsOf + Path.Interpolate
Browse files Browse the repository at this point in the history
  • Loading branch information
TylorS committed Feb 29, 2024
1 parent 02fbf69 commit 7768a3f
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 23 deletions.
13 changes: 13 additions & 0 deletions .changeset/chilly-lobsters-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@typed/path": patch
---

Improve type-level performance of `@typed/path`'s ParamsOf + Interpolate types.

This removes the need for @ts-expect-error for possibly infinite types.

For ParamsOf this was accomplished by switching from a tuple/reduce-based type for creating an intersection of types to a more
"standard" UnionToIntersection which works by changing the variance.

For Interpolate this was accomplished by using a type-level map to
allow TypeScript to narrow the problem space to only the type-level AST types used internally for parsing a path without the need of constraining the input values and dealing with type-level casts.
46 changes: 30 additions & 16 deletions packages/path/src/Interpolate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
PrefixNode,
QueryParamNode,
QueryParamsNode,
SegmentAST,
SuffixNode,
TextNode,
UnnamedParamNode
Expand All @@ -25,7 +26,7 @@ import type { PathJoin } from "./PathJoin.js"
* Interpolate a path with parameters
* @since 1.0.0
*/
export type Interpolate<P extends string, Params extends ParamsOf<P>> = A.Equals<P, string> extends 1 ? string :
export type Interpolate<P extends string, Params extends ParamsOf<P>> = A.Equals<[P], [string]> extends 1 ? string :
PathJoin<
InterpolateParts<
ParseSegments<PathToSegments<P>>,
Expand All @@ -40,23 +41,36 @@ type InterpolateParts<
readonly [K in keyof T]: InterpolatePart<T[K], Params>
}

type InterpolatePart<T, Params extends {}, IsOptional extends boolean = false, Prefix extends string = "/"> = T extends
ModifierNode<infer M, infer S> ? InterpolatePart<S, Params, M extends "+" ? false : true>
: T extends TextNode<infer T> ? T
: T extends NamedParamNode<infer N> ?
N extends keyof Params ? EnsureIsString<Params[N], Prefix> : IsOptional extends true ? "" : string
: T extends NamedParamNodeWithRegex<infer N, infer _> ?
type InterpolatePartMap<T, Params extends {}, IsOptional extends boolean, Prefix extends string> = {
Modifier: T extends ModifierNode<infer M, infer S> ? InterpolatePart<S, Params, M extends "+" ? false : true>
: never
Text: T extends TextNode<infer T> ? T : never
NamedParam: T extends NamedParamNode<infer N> ? N extends keyof Params ? EnsureIsString<Params[N], Prefix>
: IsOptional extends true ? ""
: string
: never
NamedParamWithRegex: T extends NamedParamNodeWithRegex<infer N, infer _> ?
N extends keyof Params ? EnsureIsString<Params[N], Prefix> : IsOptional extends true ? "" : string
: T extends UnnamedParamNode<infer I extends Extract<keyof Params, number>> ? EnsureIsString<Params[I], Prefix>
: T extends PrefixNode<infer P, infer S> ? // @ts-expect-error Type potentially infinite
S extends PrefixNode<infer P2, any> ? `${P}${InterpolatePart<S, Params, IsOptional, P2>}`
: `` extends InterpolatePart<S, Params, IsOptional, P> ? ``
: never
UnnamedParam: T extends UnnamedParamNode<infer I extends Extract<keyof Params, number>> ?
EnsureIsString<Params[I], Prefix>
: IsOptional extends true ? ""
: never
Prefix: T extends PrefixNode<infer P, infer S> ?
S extends PrefixNode<infer P2, infer _> ? `${P}${InterpolatePart<S, Params, IsOptional, P2>}` :
`` extends InterpolatePart<S, Params, IsOptional, P> ? ``
: `${P}${InterpolatePart<S, Params, IsOptional, P>}`
: T extends SuffixNode<infer S, infer P> ?
`` extends InterpolatePart<S, Params, IsOptional> ? `` : `${InterpolatePart<S, Params, IsOptional>}${P}`
: T extends QueryParamsNode<infer Q> ? `?${InterpolateQueryParams<Q, Params>}`
: T extends QueryParamNode<infer K, infer V> ? `${K}=${InterpolatePart<V, Params>}`
: ""
: never
Suffix: T extends SuffixNode<infer S, infer P> ? `` extends InterpolatePart<S, Params, IsOptional> ? ``
: `${InterpolatePart<S, Params, IsOptional>}${P}`
: never
QueryParams: T extends QueryParamsNode<infer Q> ? `?${InterpolateQueryParams<Q, Params>}` : never
QueryParam: T extends QueryParamNode<infer K, infer V> ? `${K}=${InterpolatePart<V, Params>}` : never
}

type InterpolatePart<T, Params extends {}, IsOptional extends boolean = false, Prefix extends string = "/"> = T extends
SegmentAST | QueryParamNode<any, any> ? InterpolatePartMap<T, Params, IsOptional, Prefix>[T["_tag"]]
: never

type InterpolateQueryParams<T extends ReadonlyArray<any>, Params extends {}> = S.Join<
{
Expand Down
12 changes: 5 additions & 7 deletions packages/path/src/ParamsOf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,18 @@ import type {
* Extract the parameters from a path
* @since 1.0.0
*/
export type ParamsOf<T extends string> = A.Equals<T, string> extends 1 ? {} : ToParams<ParseSegments<PathToSegments<T>>>
export type ParamsOf<T extends string> = A.Equals<[T], [string]> extends 1 ? {}
: ToParams<ParseSegments<PathToSegments<T>>>

type ToParams<T extends ReadonlyArray<any>, Params = {}> = [
// @ts-expect-error Type potentially infinite
ListToIntersection<
UnionToIntersection<
{
[K in keyof T]: AstToParams<T[K], Params>
}
}[number]
>
] extends [infer P] ? { readonly [K in keyof P]: P[K] } : never

type ListToIntersection<T extends ReadonlyArray<any>> = T extends readonly [infer Head, ...infer Tail]
? Head & ListToIntersection<Tail>
: unknown
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never

type AstToParams<T, Params = {}> = T extends ModifierNode<infer M, infer S> ? ModifyParams<M, AstToParams<S>> :
T extends TextNode<infer _> ? {} :
Expand Down

0 comments on commit 7768a3f

Please sign in to comment.