Skip to content

Commit

Permalink
simplify extract param type, move check for optional
Browse files Browse the repository at this point in the history
  • Loading branch information
stackoverfloweth committed Feb 5, 2025
1 parent d74174b commit 64ce2bf
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 56 deletions.
53 changes: 43 additions & 10 deletions src/types/params.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { LiteralParam, Param, ParamGetSet, ParamGetter } from '@/types/paramTypes'
import { Identity } from '@/types/utilities'
import { MakeOptional } from '@/utilities/makeOptional'
import { ExtractParamTypeWithoutLosingOptional } from '@/types/routeWithParams'
import { ZodSchema } from 'zod'

export const paramStart = '['
export type ParamStart = typeof paramStart
Expand Down Expand Up @@ -87,33 +87,66 @@ export type ExtractWithParamsParamType<
* @returns A record of parameter names to their respective types, extracted and merged from both path and query parameters.
*/
export type ExtractRouteParamTypes<TRoute> = TRoute extends {
host: { params: infer HostParams extends Record<string, Param> },
path: { params: infer PathParams extends Record<string, Param> },
query: { params: infer QueryParams extends Record<string, Param> },
host: { params: infer HostParams extends Record<string, Param> },
hash: { params: infer HashParams extends Record<string, Param> },
}
? ExtractParamTypes<HostParams & PathParams & QueryParams & HashParams>
: {}
: Record<string, unknown>

/**
* Extracts combined types of path and query parameters for a given route, creating a unified parameter object.
* This parameter object type represents the expected type when accessing params from router.route or useRoute.
* @template TRoute - The route type from which to extract and merge parameter types.
* @returns A record of parameter names to their respective types, extracted and merged from both path and query parameters.
*/
export type ExtractRouteParamTypesWithOptional<TRoute> = TRoute extends {
host: { params: infer HostParams extends Record<string, Param> },
path: { params: infer PathParams extends Record<string, Param> },
query: { params: infer QueryParams extends Record<string, Param> },
hash: { params: infer HashParams extends Record<string, Param> },
}
? ExtractParamTypesWithOptional<HostParams & PathParams & QueryParams & HashParams>
: Record<string, unknown>

/**
* Transforms a record of parameter types into a type with optional properties where the original type allows undefined.
* @template TParams - The record of parameter types, possibly including undefined.
* @returns A new type with the appropriate properties marked as optional.
*/
export type ExtractParamTypes<TParams extends Record<string, Param>> = Identity<MakeOptional<{
[K in keyof TParams as ExtractParamName<K>]: ExtractParamType<TParams[K], K>
[K in keyof TParams as ExtractParamName<K>]: ExtractParamType<TParams[K]>
}>>

/**
* Transforms a record of parameter types into a type with optional properties where the original type allows undefined.
* @template TParams - The record of parameter types, possibly including undefined.
* @returns A new type with the appropriate properties marked as optional.
*/
export type ExtractParamTypesWithOptional<TParams extends Record<string, Param>> = Identity<MakeOptional<{
[K in keyof TParams as ExtractParamName<K>]: K extends `?${string}`
? TParams[K] extends Required<ParamGetSet>
? ExtractParamType<TParams[K]>
: ExtractParamType<TParams[K]> | undefined
: ExtractParamType<TParams[K]>
}>>

/**
* Extracts the actual type from a parameter type, handling getters, setters, and potential undefined values.
* This type also is responsible for narrowing possibly undefined values when the param has a default value.
* Extracts the actual type from a parameter type, handling getters and setters.
* @template TParam - The parameter type.
* @returns The extracted type, or 'string' as a fallback.
*/
export type ExtractParamType<TParam extends Param, TParamKey extends PropertyKey = string> =
TParam extends Required<ParamGetSet>
? Exclude<ExtractParamTypeWithoutLosingOptional<TParam, TParamKey>, undefined>
: ExtractParamTypeWithoutLosingOptional<TParam, TParamKey>
export type ExtractParamType<TParam extends Param> =
TParam extends ParamGetSet<infer Type>
? Type
: TParam extends ParamGetter
? ReturnType<TParam>
: TParam extends ZodSchema<infer Type>
? Type
: TParam extends LiteralParam
? TParam
: string

type RemoveLeadingQuestionMark<T extends PropertyKey> = T extends `?${infer TRest extends string}` ? TRest : T
export type RemoveLeadingQuestionMarkFromKeys<T extends Record<string, unknown>> = {
Expand Down
4 changes: 2 additions & 2 deletions src/types/resolved.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ExtractRouteParamTypes } from '@/types/params'
import { ExtractRouteParamTypesWithOptional } from '@/types/params'
import { Route } from '@/types/route'
import { ExtractRouteStateParamsAsOptional } from '@/types/state'
import { Url } from '@/types/url'
Expand Down Expand Up @@ -36,7 +36,7 @@ export type ResolvedRoute<TRoute extends Route = Route> = Readonly<{
/**
* Key value pair for route params, values will be the user provided value from current browser location.
*/
params: ExtractRouteParamTypes<TRoute>,
params: ExtractRouteParamTypesWithOptional<TRoute>,
/**
* Type for additional data intended to be stored in history state.
*/
Expand Down
4 changes: 2 additions & 2 deletions src/types/route.spec-d.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { describe, expectTypeOf, test } from 'vitest'
import { ExtractRouteParamTypes } from '@/types/params'
import { ExtractRouteParamTypesWithOptional } from '@/types/params'
import { Route } from '@/types/route'
import { WithParams } from '@/services/withParams'

describe('ExtractRouteParamTypes', () => {
test('given routes with different params, some optional, combines into expected args for developer', () => {
type TestRoute = Route<'parentA', WithParams<'https://[inHost].dev', {}>, WithParams<'/[inPath]', {}>, WithParams<'foo=[inQuery]&bar=[?paramC]', { inQuery: BooleanConstructor }>, WithParams<'[inHash]', {}>>

type Source = ExtractRouteParamTypes<TestRoute>
type Source = ExtractRouteParamTypesWithOptional<TestRoute>
type Expect = { inHost: string, inPath: string, paramC?: string, inQuery: boolean, inHash: string }

expectTypeOf<Source>().toEqualTypeOf<Expect>()
Expand Down
42 changes: 2 additions & 40 deletions src/types/routeWithParams.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,10 @@
import { ExtractParamName } from '@/types/params'
import { LiteralParam, Param, ParamGetSet, ParamGetter } from '@/types/paramTypes'
import { ExtractRouteParamTypesWithOptional } from '@/types/params'
import { Routes } from '@/types/route'
import { RoutesName, RoutesMap } from '@/types/routesMap'
import { Identity } from '@/types/utilities'
import { MakeOptional } from '@/utilities/makeOptional'
import type { ZodSchema } from 'zod'

export type RouteGetByKey<TRoutes extends Routes, TKey extends RoutesName<TRoutes>> = RoutesMap<TRoutes>[TKey]

export type RouteParamsByKey<
TRoutes extends Routes,
TKey extends string
> = ExtractRouteParamTypesWithoutLosingOptional<RouteGetByKey<TRoutes, TKey>>

type ExtractRouteParamTypesWithoutLosingOptional<TRoute> = TRoute extends {
host: { params: infer HostParams extends Record<string, Param> },
path: { params: infer PathParams extends Record<string, Param> },
query: { params: infer QueryParams extends Record<string, Param> },
hash: { params: infer HashParams extends Record<string, Param> },
}
? ExtractParamTypesWithoutLosingOptional<HostParams & PathParams & QueryParams & HashParams>
: Record<string, unknown>

type ExtractParamTypesWithoutLosingOptional<TParams extends Record<string, Param>> = Identity<MakeOptional<{
[K in keyof TParams as ExtractParamName<K>]: ExtractParamTypeWithoutLosingOptional<TParams[K], K>
}>>

export type ExtractParamTypeWithoutLosingOptional<TParam extends Param, TParamKey extends PropertyKey> =
TParam extends ParamGetSet<infer Type>
? TParamKey extends `?${string}`
? Type | undefined
: Type
: TParam extends ParamGetter
? TParamKey extends `?${string}`
? ReturnType<TParam> | undefined
: ReturnType<TParam>
: TParam extends ZodSchema<infer Type>
? TParamKey extends `?${string}`
? Type | undefined
: Type
: TParam extends LiteralParam
? TParamKey extends `?${string}`
? TParam | undefined
: TParam
: TParamKey extends `?${string}`
? string | undefined
: string
> = ExtractRouteParamTypesWithOptional<RouteGetByKey<TRoutes, TKey>>
4 changes: 2 additions & 2 deletions src/types/state.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { ExtractParamTypes } from '@/types/params'
import { ExtractParamTypes, ExtractParamTypesWithOptional } from '@/types/params'
import { Param } from '@/types/paramTypes'
import { Routes } from '@/types/route'
import { RouteGetByKey } from '@/types/routeWithParams'

export type ToState<TState extends Record<string, Param> | undefined> = TState extends undefined ? Record<string, Param> : unknown extends TState ? {} : TState

export type ExtractRouteStateParamsAsOptional<T extends Record<string, Param>> = ExtractParamTypes<{
export type ExtractRouteStateParamsAsOptional<T extends Record<string, Param>> = ExtractParamTypesWithOptional<{
[K in keyof T as K extends string ? `?${K}` : never]: T[K]
}>

Expand Down

0 comments on commit 64ce2bf

Please sign in to comment.