Summary
InferRoutePatternSegment in packages/fresh/src/application/route/types.ts:114 returns EmptyRecord (= Record<string, never>) for static route segments. When intersected via & in InferRoutePatternPathSegments, the index signature [k: string]: never collapses all dynamic-segment fields to never. This makes createRouteReference("/channel/[id]").href({ path: { id: "c-123" } }) fail with TS2322: Type "{ id: string; }" is not assignable to type "never".
Confirmed reproduction against @netscript/fresh@0.0.1-alpha.12:
TS2322 [ERROR]: Type "{ id: string; }" is not assignable to type "never".
const ok: ChannelPathInput = { id: "c-123" };
Related
Root cause
// packages/fresh/src/application/route/types.ts:110-121 (CURRENT — buggy)
type InferRoutePatternSegment<TSegment extends string> =
TSegment extends `[[...${infer TParam}]]`
? { [TKey in TParam]?: readonly string[] }
: TSegment extends `[...${infer TParam}]` ? { [TKey in TParam]: readonly string[] }
: TSegment extends `[${infer TParam}]` ? { [TKey in TParam]: string }
: EmptyRecord; // BUG: index signature narrows dynamic fields to never
type InferRoutePatternPathSegments<TPattern extends string> =
TPattern extends `${infer TSegment}/${infer TRest}`
? InferRoutePatternSegment<TSegment> & InferRoutePatternPathSegments<TRest> // missing Simplify<>
: InferRoutePatternSegment<TPattern>;
Same bug mirrored in packages/fresh/src/application/route/_internal/contract-types.ts:74. The internal file already wraps the intersection in Simplify<> at line 78 — only the EmptyRecord → {} change is needed there.
Prior-repo comparison
netscript-start/packages/fresh/route/contract.ts:65-74 (the previous version) had the correct implementation already:
type InferRoutePatternSegment<TSegment extends string> =
TSegment extends `[[...${infer TParam}]]`
? { [TKey in TParam]?: readonly string[] }
: TSegment extends `[...${infer TParam}]` ? { [TKey in TParam]: readonly string[] }
: TSegment extends `[${infer TParam}]` ? { [TKey in TParam]: string }
: {}; // was already {}
type InferRoutePatternPathSegments<TPattern extends string> =
TPattern extends `${infer TSegment}/${infer TRest}`
? Simplify<InferRoutePatternSegment<TSegment> & InferRoutePatternPathSegments<TRest>> // was already Simplify<>
: InferRoutePatternSegment<TPattern>;
This is a regression introduced during the netscript-start → netscript port, not a design change. The fix is to restore the prior implementation.
Proposed fix
Two-line patch per file (do NOT change EmptyRecord itself — it is used as the "no path params" discriminator at types.ts:162-171 and many other places):
packages/fresh/src/application/route/types.ts
type InferRoutePatternSegment<TSegment extends string> =
TSegment extends `[[...${infer TParam}]]`
? { [TKey in TParam]?: readonly string[] }
: TSegment extends `[...${infer TParam}]` ? { [TKey in TParam]: readonly string[] }
: TSegment extends `[${infer TParam}]` ? { [TKey in TParam]: string }
- : EmptyRecord;
+ : {};
type InferRoutePatternPathSegments<TPattern extends string> =
TPattern extends `${infer TSegment}/${infer TRest}`
- ? InferRoutePatternSegment<TSegment> & InferRoutePatternPathSegments<TRest>
+ ? Simplify<InferRoutePatternSegment<TSegment> & InferRoutePatternPathSegments<TRest>>
: InferRoutePatternSegment<TPattern>;
packages/fresh/src/application/route/_internal/contract-types.ts
type InferRoutePatternSegment<TSegment extends string> =
TSegment extends `[[...${infer TParam}]]`
? { [TKey in TParam]?: readonly string[] }
: TSegment extends `[...${infer TParam}]` ? { [TKey in TParam]: readonly string[] }
: TSegment extends `[${infer TParam}]` ? { [TKey in TParam]: string }
- : EmptyRecord;
+ : {};
(Line 78 already has Simplify<>, no change needed there.)
Validation cases
The fix must keep these six cases green at type-check:
| Pattern |
Expected InferRoutePatternPath |
/channel/[id] |
{ id: string } |
/session/[a]/[b] |
{ a: string; b: string } |
/[id] (single dynamic, no static prefix) |
{ id: string } (no regression) |
/static/only |
{} (no regression — framework collapses to never for static href() input) |
/posts/[[...slug]] |
{ slug?: readonly string[] } (no regression) |
/dashboard/products/[id]/edit (existing internal test case) |
{ id: string } |
Plus the runtime cases from contract.test.ts:176-235 must keep passing.
Tasks
Cross-link to WI-12
Once this lands, WI-12's codegen work can proceed without the underlying type-inference regression breaking playground routes/(dashboard)/dashboard/orders/[id].tsx and similar multi-segment dynamic routes. Without this fix, WI-12's typed Link integration would surface the same TS2322 to every page module with a dynamic segment.
Summary
InferRoutePatternSegmentinpackages/fresh/src/application/route/types.ts:114returnsEmptyRecord(=Record<string, never>) for static route segments. When intersected via&inInferRoutePatternPathSegments, the index signature[k: string]: nevercollapses all dynamic-segment fields tonever. This makescreateRouteReference("/channel/[id]").href({ path: { id: "c-123" } })fail withTS2322: Type "{ id: string; }" is not assignable to type "never".Confirmed reproduction against
@netscript/fresh@0.0.1-alpha.12:Related
neverpath params for multi-segment patterns (EmptyRecord intersection) #177 — original user report, contains the analysis chain. This issue is the implementation tracking entry for the fix.WI-12-prompt.md) — codegen work that emits.withRoute(routes.<key>.$route)for page modules. WI-12 depends on this fix landing first, otherwise playgroundroutes/(dashboard)/dashboard/orders/[id].tsxconsumers will hit the sameTS2322once typedLink/hrefis wired.Root cause
Same bug mirrored in
packages/fresh/src/application/route/_internal/contract-types.ts:74. The internal file already wraps the intersection inSimplify<>at line 78 — only theEmptyRecord→{}change is needed there.Prior-repo comparison
netscript-start/packages/fresh/route/contract.ts:65-74(the previous version) had the correct implementation already:This is a regression introduced during the netscript-start → netscript port, not a design change. The fix is to restore the prior implementation.
Proposed fix
Two-line patch per file (do NOT change
EmptyRecorditself — it is used as the "no path params" discriminator attypes.ts:162-171and many other places):packages/fresh/src/application/route/types.tstype InferRoutePatternSegment<TSegment extends string> = TSegment extends `[[...${infer TParam}]]` ? { [TKey in TParam]?: readonly string[] } : TSegment extends `[...${infer TParam}]` ? { [TKey in TParam]: readonly string[] } : TSegment extends `[${infer TParam}]` ? { [TKey in TParam]: string } - : EmptyRecord; + : {}; type InferRoutePatternPathSegments<TPattern extends string> = TPattern extends `${infer TSegment}/${infer TRest}` - ? InferRoutePatternSegment<TSegment> & InferRoutePatternPathSegments<TRest> + ? Simplify<InferRoutePatternSegment<TSegment> & InferRoutePatternPathSegments<TRest>> : InferRoutePatternSegment<TPattern>;packages/fresh/src/application/route/_internal/contract-types.tstype InferRoutePatternSegment<TSegment extends string> = TSegment extends `[[...${infer TParam}]]` ? { [TKey in TParam]?: readonly string[] } : TSegment extends `[...${infer TParam}]` ? { [TKey in TParam]: readonly string[] } : TSegment extends `[${infer TParam}]` ? { [TKey in TParam]: string } - : EmptyRecord; + : {};(Line 78 already has
Simplify<>, no change needed there.)Validation cases
The fix must keep these six cases green at type-check:
InferRoutePatternPath/channel/[id]{ id: string }/session/[a]/[b]{ a: string; b: string }/[id](single dynamic, no static prefix){ id: string }(no regression)/static/only{}(no regression — framework collapses toneverfor static href() input)/posts/[[...slug]]{ slug?: readonly string[] }(no regression)/dashboard/products/[id]/edit(existing internal test case){ id: string }Plus the runtime cases from
contract.test.ts:176-235must keep passing.Tasks
packages/fresh/src/application/route/types.tspackages/fresh/src/application/route/_internal/contract-types.tspackages/fresh/src/application/route/contract.test.tscovering all six patterns above (the existing test at line 176 usesas unknown ascasts that mask the bug — extend it with non-cast assertions)deno task check:packages— must stay greendeno check --config deno.json apps/playground— must stay green (this is the integration gate that catches the WI-12 cross-link)Cross-link to WI-12
Once this lands, WI-12's codegen work can proceed without the underlying type-inference regression breaking playground
routes/(dashboard)/dashboard/orders/[id].tsxand similar multi-segment dynamic routes. Without this fix, WI-12's typedLinkintegration would surface the sameTS2322to every page module with a dynamic segment.