Skip to content

const type parameter doesn't capture string-literal property when the parameter type is wrapped in Omit<...> #4391

Description

@IanVS

Note: I used Claude to troubleshoot and craft the repro and description.

Steps to reproduce

A const type parameter on an interface call signature stops capturing a string-literal property from the argument object when the parameter's type is wrapped in a homomorphic mapped type (Omit<...>). The literal widens to string, collapsing a downstream conditional type and producing a spurious TS2353.

Minimal repro (depends on TanStack Router, which uses this pattern for its route-bound Route.redirect, whose options type is Omit<RedirectOptions<…>, 'from'>): https://github.com/IanVS/tsgo-error-repro

repro.ts

import { createRootRoute, createRoute, createRouter, redirect } from "@tanstack/react-router";

const rootRoute = createRootRoute();
const agentRoute = createRoute({ getParentRoute: () => rootRoute, path: "/agents/$agentName" });
const agentIndexRoute = createRoute({ getParentRoute: () => agentRoute, path: "/" });
const sessionRoute = createRoute({ getParentRoute: () => agentRoute, path: "/$sessionId" });
const routeTree = rootRoute.addChildren([agentRoute.addChildren([agentIndexRoute, sessionRoute])]);
const router = createRouter({ routeTree });
declare module "@tanstack/react-router" {
  interface Register { router: typeof router; }
}

// A) plain redirect, no `from` — OK on both compilers
redirect({
  to: "/agents/$agentName/$sessionId",
  params: { agentName: "a", sessionId: "s" },
});

// B) plain redirect WITH explicit `from` (options NOT Omit-wrapped) — OK on both compilers
redirect({
  from: "/agents/$agentName/",
  to: "/agents/$agentName/$sessionId",
  params: { agentName: "a", sessionId: "s" },
});

// C) route-bound redirect — options type is Omit<RedirectOptions<…>, "from">
agentIndexRoute.redirect({
  to: "/agents/$agentName/$sessionId",
  params: { agentName: "a", sessionId: "s" },
});

All three calls are valid: sessionId is a real path param of /agents/$agentName/$sessionId, supplied correctly. The only difference between case B (accepted everywhere) and case C (rejected by tsgo) is that C's options parameter is typed Omit<RedirectOptions<…>, 'from'> instead of an unwrapped object — isolating the mapped-type wrapper as the trigger.

The relevant declaration in @tanstack/router-core:

interface RedirectFnRoute<in out TDefaultFrom extends string = string> {
  <TRouter extends AnyRouter, const TTo extends string | undefined = undefined, ...>(
    opts: Omit<RedirectOptions<TRouter, TDefaultFrom, TTo, ...>, 'from'>   // <-- mapped type
  ): Redirect<...>;
}

Behavior with typescript@6.0

No errors (verified with typescript@6.0.3). All three calls type-check.

Behavior with tsgo

Case C is rejected:

repro.ts(31,29): error TS2353: Object literal may only specify known properties, and 'sessionId'
does not exist in type 'ParamsReducerFn<RouterCore<…>, ...> | { ... }'.

The to-dependent type parameter resolves as string rather than the literal "/agents/$agentName/$sessionId", so the params type collapses to the parent route's params ({ agentName }) and sessionId is rejected.

Regression bisect (@typescript/native-preview):

Build Result
7.0.0-dev.20260612.1 OK
7.0.0-dev.20260613.1 TS2353 (first bad build)
7.0.0-dev.20260617.2 TS2353
7.0.0-dev.20260622.1 TS2353 (latest)

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions