T
TanStack8mo ago
correct-apricot

Link `to` prefix helper and typescript

My application supports a single to have access to multiple teams. Almost all links in the application will be links to the same /team/$slug prefix. I am trying to build a Link helper but I am struggling in navigating the types to play nice with my component. Any direction would be appreciated. Example component:
import { Link, LinkComponentProps } from "@tanstack/react-router";
import { useTeam } from "~/hooks/use-team";

export const TeamLink: React.FC<
LinkComponentProps<"a"> & {
params: Record<string, unknown>;
}
> = ({ to, params, ...props }) => {
const { team } = useTeam();
return (
<Link
to={`/team/$teamSlug${to}`}
params={{ ...params, teamSlug: team.slug }}
{...props}
>
{props.children}
</Link>
);
};
import { Link, LinkComponentProps } from "@tanstack/react-router";
import { useTeam } from "~/hooks/use-team";

export const TeamLink: React.FC<
LinkComponentProps<"a"> & {
params: Record<string, unknown>;
}
> = ({ to, params, ...props }) => {
const { team } = useTeam();
return (
<Link
to={`/team/$teamSlug${to}`}
params={{ ...params, teamSlug: team.slug }}
{...props}
>
{props.children}
</Link>
);
};
5 Replies
multiple-amethyst
multiple-amethyst8mo ago
Custom Link | TanStack Router React Docs
While repeating yourself can be acceptable in many situations, you might find that you do it too often. At times, you may want to create cross-cutting components with additional behavior or styles. Yo...
multiple-amethyst
multiple-amethyst8mo ago
btw looks like you need a relative link
correct-apricot
correct-apricotOP8mo ago
Thanks for the quick response. I did check the docs. Those work great for adding props to the resulting element, but do not help much in constructing a to value thats typed appropriately. But you know what, as alwasy when you ask a question the answer may come to you. GIve me a few min and get back. Im 1000% sure there is a better way but in case anyone else stubles across this, its how I unblocked myself.
import { Link, LinkComponentProps } from "@tanstack/react-router";
import { useTeam } from "~/hooks/use-team";

type LinkTo = LinkComponentProps<"a">["to"];
type WithoutTeamPrefix<T> = T extends `/team/$teamSlug${infer Rest}`
? Rest
: never;
type NewUnionType = WithoutTeamPrefix<LinkTo>;

type LinkParams = LinkComponentProps<"a">["params"];
type WithoutTeamSlugParams<T> = T extends { teamSlug?: string | undefined }
? Omit<T, "teamSlug">
: T; // Fallback to T if it doesn't match

type NewLinkParams = LinkParams extends infer U
? U extends any
? WithoutTeamSlugParams<U>
: never
: never;

export type TeamLinkProps = Omit<LinkComponentProps<"a">, "to" | "params"> & {
to: NewUnionType;
params: NewLinkParams;
};

export const TeamLink: React.FC<TeamLinkProps> = ({ to, params, ...props }) => {
const { team } = useTeam();
return (
<Link
to={`/team/$teamSlug${to}` as LinkTo}
params={{ ...(params as object), teamSlug: team.slug }}
{...props}
>
{props.children}
</Link>
);
};
import { Link, LinkComponentProps } from "@tanstack/react-router";
import { useTeam } from "~/hooks/use-team";

type LinkTo = LinkComponentProps<"a">["to"];
type WithoutTeamPrefix<T> = T extends `/team/$teamSlug${infer Rest}`
? Rest
: never;
type NewUnionType = WithoutTeamPrefix<LinkTo>;

type LinkParams = LinkComponentProps<"a">["params"];
type WithoutTeamSlugParams<T> = T extends { teamSlug?: string | undefined }
? Omit<T, "teamSlug">
: T; // Fallback to T if it doesn't match

type NewLinkParams = LinkParams extends infer U
? U extends any
? WithoutTeamSlugParams<U>
: never
: never;

export type TeamLinkProps = Omit<LinkComponentProps<"a">, "to" | "params"> & {
to: NewUnionType;
params: NewLinkParams;
};

export const TeamLink: React.FC<TeamLinkProps> = ({ to, params, ...props }) => {
const { team } = useTeam();
return (
<Link
to={`/team/$teamSlug${to}` as LinkTo}
params={{ ...(params as object), teamSlug: team.slug }}
{...props}
>
{props.children}
</Link>
);
};
multiple-amethyst
multiple-amethyst8mo ago
how about
import {
AnyRouter,
Constrain,
InferMaskFrom,
InferMaskTo,
InferTo,
Link,
LinkComponentProps,
RegisteredRouter,
} from '@tanstack/react-router';

function useTeam() {
return { team: { slug: 'slug' } };
}

export type ValidateLinkOptions<
TOptions,
TComp = 'a',
TRouter extends AnyRouter = RegisteredRouter
> = Constrain<
TOptions,
LinkComponentProps<
TComp,
TRouter,
'/team/$teamSlug',
InferTo<TOptions>,
InferMaskFrom<TOptions>,
InferMaskTo<TOptions>
>
>;

export function TeamLink<TOptions>(
props: ValidateLinkOptions<TOptions> & { children: React.ReactNode }
) {
const { team } = useTeam();

return (
<Link
from="/team/$teamSlug"
{...props}
params={{ teamSlug: team.slug, ...props }}
>
{props.children}
</Link>
);
}

function Foo() {
return (
<TeamLink to="./$some/xyz" params={{ some: 'foo' }}>
hello team
</TeamLink>
);
}
import {
AnyRouter,
Constrain,
InferMaskFrom,
InferMaskTo,
InferTo,
Link,
LinkComponentProps,
RegisteredRouter,
} from '@tanstack/react-router';

function useTeam() {
return { team: { slug: 'slug' } };
}

export type ValidateLinkOptions<
TOptions,
TComp = 'a',
TRouter extends AnyRouter = RegisteredRouter
> = Constrain<
TOptions,
LinkComponentProps<
TComp,
TRouter,
'/team/$teamSlug',
InferTo<TOptions>,
InferMaskFrom<TOptions>,
InferMaskTo<TOptions>
>
>;

export function TeamLink<TOptions>(
props: ValidateLinkOptions<TOptions> & { children: React.ReactNode }
) {
const { team } = useTeam();

return (
<Link
from="/team/$teamSlug"
{...props}
params={{ teamSlug: team.slug, ...props }}
>
{props.children}
</Link>
);
}

function Foo() {
return (
<TeamLink to="./$some/xyz" params={{ some: 'foo' }}>
hello team
</TeamLink>
);
}
@Chris Horobin can we allow to specify e.g. TFrom for ValidateLinkOptions ? I just duplicated the type, but I hope there is a solution with even less code
eastern-cyan
eastern-cyan8mo ago
You can. You just need an intersection. ValidateLinkOptions<TOptions & { from: TFrom }>

Did you find this page helpful?