Summary
createRouteReference('/a/[id]') infers a broken path-param type for any pattern that has a static segment before a dynamic one. The reference works at runtime (string substitution is correct), but .href({ path: { id } }) fails to type-check because the inferred path type collapses the dynamic param to never.
Repro
import { createRouteReference } from '@netscript/fresh/route';
const channel = createRouteReference('/channel/[id]', { id: 'channel', kind: 'page' });
// Runtime: correct → "/channel/c-123"
channel.href({ path: { id: 'c-123' } } as never);
// Type-check: ERROR
channel.href({ path: { id: 'c-123' } });
// TS2322: Type '{ id: string; }' is not assignable to type 'EmptyRecord & { id: string; }'.
// Property 'id' is incompatible with index signature.
// Type 'string' is not assignable to type 'never'.
(@netscript/fresh@0.0.1-alpha.12, Deno 2.9.)
Root cause
From deno doc @netscript/fresh/route:
type InferRoutePatternPathSegments<TPattern> =
TPattern extends '' ? EmptyRecord
: TPattern extends `${infer TSegment}/${infer TRest}`
? InferRoutePatternSegment<TSegment> & InferRoutePatternPathSegments<TRest>
: InferRoutePatternSegment<TPattern>;
type InferRoutePatternSegment<TSegment> =
TSegment extends `[[...${infer P}]]` ? { [K in P]?: readonly string[] }
: TSegment extends `[...${infer P}]` ? { [K in P]: readonly string[] }
: TSegment extends `[${infer P}]` ? { [K in P]: string }
: EmptyRecord;
For /channel/[id] → strip slash → channel/[id] → InferRoutePatternSegment<'channel'> & InferRoutePatternPathSegments<'[id]'> = EmptyRecord & { id: string }.
EmptyRecord is Record<string, never> (has an index signature [k: string]: never). Intersecting it with { id: string } forces id: string & never = never, so any value for id is rejected. Every multi-segment route with a leading static segment (the common case — /channel/[id], /session/[a]/[b], …) is affected. A single-segment dynamic pattern (/[id]) is fine because there is no static EmptyRecord to intersect.
Suggested fix
Make static segments contribute {} (no index signature) instead of EmptyRecord, or accumulate only dynamic segments so the intersection never includes an index-signature type. Either of these makes /channel/[id] infer { id: string }:
// static segment → {} (not Record<string, never>)
type InferRoutePatternSegment<TSegment> =
TSegment extends `[[...${infer P}]]` ? { [K in P]?: readonly string[] }
: TSegment extends `[...${infer P}]` ? { [K in P]: readonly string[] }
: TSegment extends `[${infer P}]` ? { [K in P]: string }
: {}; // ← was EmptyRecord
(Then Prettify/clean the final intersection if you want a flat object type.)
Workaround in the meantime
Re-typing the reference with the intended path shape (runtime is already correct):
function pathRef<TPath extends object>(pattern: string, meta: { id: string; kind: 'page' }) {
return createRouteReference(pattern, meta) as unknown as RouteReference<TPath, SearchParamInput>;
}
const channel = pathRef<{ id: string }>('/channel/[id]', { id: 'channel', kind: 'page' });
Secondary question (docs)
The routing docs imply withRoute(createRouteReference('/a/[id]')) yields a typed ctx.path, but an inference-only reference appears to carry no runtime path schema, so ctx.path is empty in the page loader (we read ctx.params.id instead). If that's expected, a note in web-layer/route.md distinguishing "typed outbound href" from "inbound ctx.path requires an explicit pathSchema/withParams" would help — happy to PR the doc clarification.
Summary
createRouteReference('/a/[id]')infers a broken path-param type for any pattern that has a static segment before a dynamic one. The reference works at runtime (string substitution is correct), but.href({ path: { id } })fails to type-check because the inferred path type collapses the dynamic param tonever.Repro
(
@netscript/fresh@0.0.1-alpha.12, Deno 2.9.)Root cause
From
deno doc @netscript/fresh/route:For
/channel/[id]→ strip slash →channel/[id]→InferRoutePatternSegment<'channel'> & InferRoutePatternPathSegments<'[id]'>=EmptyRecord & { id: string }.EmptyRecordisRecord<string, never>(has an index signature[k: string]: never). Intersecting it with{ id: string }forcesid: string & never = never, so any value foridis rejected. Every multi-segment route with a leading static segment (the common case —/channel/[id],/session/[a]/[b], …) is affected. A single-segment dynamic pattern (/[id]) is fine because there is no staticEmptyRecordto intersect.Suggested fix
Make static segments contribute
{}(no index signature) instead ofEmptyRecord, or accumulate only dynamic segments so the intersection never includes an index-signature type. Either of these makes/channel/[id]infer{ id: string }:(Then
Prettify/clean the final intersection if you want a flat object type.)Workaround in the meantime
Re-typing the reference with the intended path shape (runtime is already correct):
Secondary question (docs)
The routing docs imply
withRoute(createRouteReference('/a/[id]'))yields a typedctx.path, but an inference-only reference appears to carry no runtime path schema, soctx.pathis empty in the page loader (we readctx.params.idinstead). If that's expected, a note inweb-layer/route.mddistinguishing "typed outboundhref" from "inboundctx.pathrequires an explicitpathSchema/withParams" would help — happy to PR the doc clarification.