next.js: Statically Typed Links not working when passing href

Verify canary release

  • I verified that the issue exists in the latest Next.js canary release

Provide environment information

Operating System:
      Platform: darwin
      Arch: x64
      Version: Darwin Kernel Version 21.6.0: Mon Dec 19 20:44:01 PST 2022; root:xnu-8020.240.18~2/RELEASE_X86_64
    Binaries:
      Node: 18.15.0
      npm: 9.5.0
      Yarn: N/A
      pnpm: N/A
    Relevant packages:
      next: 13.2.5-canary.21
      eslint-config-next: 13.2.4
      react: 18.2.0
      react-dom: 18.2.0

Which area(s) of Next.js are affected? (leave empty if unsure)

App directory (appDir: true), TypeScript

Link to the code that reproduces this issue

https://codesandbox.io/p/sandbox/angry-scott-twxtz4?file=%2F.next%2Ftypes%2Flink.d.ts&selection=[{"endColumn"%3A17%2C"endLineNumber"%3A32%2C"startColumn"%3A17%2C"startLineNumber"%3A32}]

To Reproduce

I’ve been trying server components with the new “app” directory with the typedRoutes: true config set too true.

It generates the following: routes definitions in the Next.js folder:

 type StaticRoutes = 
    | `/`
    | `/pokedex`
    | `/pokedex/create`
    | `/todo`
    | `/weather`
    | `/404.page`
    | `/404.test`
    | `/500.test`
    | `/500.page`
  type DynamicRoutes<T extends string = string> = 
    | `/pokedex/${SafeSlug<T>}`
    | `/pokedex/${SafeSlug<T>}/evolutions`
    | `/pokedex/${SafeSlug<T>}/edit`
    | `/weather/${SafeSlug<T>}`
    | `/api/${OptionalCatchAllSlug<T>}`

  type RouteImpl<T> =
    | StaticRoutes
    | `${StaticRoutes}${Suffix}`
    | (T extends `${DynamicRoutes<infer _>}${Suffix}` ? T : never)

I get a TypeScript error in a component called <Pager> it creates a two button pagination component:

import Link from 'next/link';
import { Page } from '../../types';
import { Route } from 'next';

type Props<T extends string> = {
  page: Page<unknown>;
  hrefForPage(page: string): Route<T> | URL;
};

export function Pager<T extends string>(props: Props<T>) {
  const { page } = props;

  if (page.first && page.last) {
    return null;
  }

  const prevPage = `${page.number - 1}`;
  const nextPage = `${page.number + 1}`;

  return (
    <div>
      <Link href={props.hrefForPage(prevPage)}>
        <button disabled={page.first}>prev</button>
      </Link>
      <Link href={props.hrefForPage(nextPage)}>
        <button disabled={page.last}>next</button>
      </Link>
    </div>
  );
}

The idea behind the hrefForPage is that it is a callback function, which should return the proper url for the next and previous Link, given the page number as a string.

This differs from the beta documentation example, as that example passes the href directly, with no function in between.

Note: that I also have components which do pass the href like in the documentation, and this works like a charm.

TypeScript does not however agree that the hrefForPage returns the correct type:

> tsc --version && tsc --noEmit

Version 5.0.2
src/components/Pager/Pager.tsx:22:13 - error TS2322: Type 'URL | Route<T>' is not assignable to type 'UrlObject | RouteImpl<URL>'.
  Type 'T extends `/pokedex/${SafeSlug<infer _ extends string>}` | `/pokedex/${SafeSlug<infer _ extends string>}/edit` | `/pokedex/${SafeSlug<infer _ extends string>}/evolutions` | `/weather/${SafeSlug<infer _ extends string>}` | ... 10 more ... | `/api/${OptionalCatchAllSlug<...>}#${string}` ? T : never' is not assignable to type 'UrlObject | RouteImpl<URL>'.
    Type '(`/pokedex/${string}` | `/pokedex/${string}/edit` | `/pokedex/${string}/evolutions` | `/weather/${string}` | `/api/${string}` | `/pokedex/${string}?${string}` | `/pokedex/${string}#${string}` | `/pokedex/${string}/edit?${string}` | `/pokedex/${string}/edit#${string}` | `/pokedex/${string}/evolutions?${string}` | `/p...' is not assignable to type 'UrlObject | RouteImpl<URL>'.
      Type '`/pokedex/${string}` & T' is not assignable to type 'UrlObject | RouteImpl<URL>'.
        Type '`/pokedex/${string}` & T' is not assignable to type 'UrlObject'.
          Type 'T extends `/pokedex/${SafeSlug<infer _ extends string>}` | `/pokedex/${SafeSlug<infer _ extends string>}/edit` | `/pokedex/${SafeSlug<infer _ extends string>}/evolutions` | `/weather/${SafeSlug<infer _ extends string>}` | ... 10 more ... | `/api/${OptionalCatchAllSlug<...>}#${string}` ? T : never' is not assignable to type 'UrlObject'.
            Type '(`/pokedex/${string}` | `/pokedex/${string}/edit` | `/pokedex/${string}/evolutions` | `/weather/${string}` | `/api/${string}` | `/pokedex/${string}?${string}` | `/pokedex/${string}#${string}` | `/pokedex/${string}/edit?${string}` | `/pokedex/${string}/edit#${string}` | `/pokedex/${string}/evolutions?${string}` | `/p...' is not assignable to type 'UrlObject'.
              Type '`/pokedex/${string}` & T' is not assignable to type 'UrlObject'.
                Types of property 'search' are incompatible.
                  Type '{ (regexp: string | RegExp): number; (searcher: { [Symbol.search](string: string): number; }): number; }' is not assignable to type 'string'.

22       <Link href={props.hrefForPage(prevPage)}>
               ~~~~

  .next/types/link.d.ts:78:5
    78     href: __next_route_internal_types__.RouteImpl<T> | UrlObject
           ~~~~
    The expected type comes from property 'href' which is declared here on type 'IntrinsicAttributes & LinkRestProps & { href: UrlObject | RouteImpl<URL>; }'

src/components/Pager/Pager.tsx:25:13 - error TS2322: Type 'URL | Route<T>' is not assignable to type 'UrlObject | RouteImpl<URL>'.

25       <Link href={props.hrefForPage(nextPage)}>
               ~~~~

  .next/types/link.d.ts:78:5
    78     href: __next_route_internal_types__.RouteImpl<T> | UrlObject
           ~~~~
    The expected type comes from property 'href' which is declared here on type 'IntrinsicAttributes & LinkRestProps & { href: UrlObject | RouteImpl<URL>; }'


Found 2 errors in the same file, starting at: src/components/Pager/Pager.tsx:22

Describe the Bug

TypeScript gives an error whenever a callback function returns Route<T> | URL when passing the result of that callback function to the href prop of the Link component.

Expected Behavior

I expect that TypeScript thinks that the result of a function that returns Route<T> | URL should be valid.

Which browser are you using? (if relevant)

No response

How are you deploying your application? (if relevant)

No response

About this issue

  • Original URL
  • State: open
  • Created a year ago
  • Reactions: 10
  • Comments: 22 (13 by maintainers)

Most upvoted comments

In file next.config.js, deactivate typedRoutes:

typedRoutes: false

The issue also applies to the official app-dir-i18n-routing example.

Reproduction

  1. Create a new project derived from the example
  2. Add typedRoutes: true to next.config.js
  3. TS will yell at line 23 in app/[lang]/components/locale-switcher.tsx, where a dynamic href is passed to <Link>: Type 'string' is not assignable to type 'UrlObject'.ts(2322)

Tried changes

1. Typing of inner function

When I change line 10:

- const redirectedPathName = (locale: string) => {
+ const redirectedPathName = <T extends string>(locale: string): Route<T> | URL => {

I get Errors on line 11 and 14 where strings are returned, but the Link component href is fine:

Type '"/"' is not assignable to type '(T extends `/${SafeSlug<infer _ extends string>}` | `/${SafeSlug<infer _ extends string>}?${string}` | `/${SafeSlug<infer _ extends string>}#${string}` ? T : never) | URL'.ts(2322)

2. Typing of Link component

When I change line 8 and line 24:

- export default function LocaleSwitcher() {
+ export default function LocaleSwitcher<T extends string>() {
...
- <Link href={redirectedPathName(locale)}>{locale}</Link>
+ <Link<Route<T> | URL> href={redirectedPathName(locale)}>{locale}</Link>

I get a similar error to the original one (shorted version copied from VSCode tooltip):

Type 'string' is not assignable to type 'UrlObject | ((T extends `/${SafeSlug<infer _ extends string>}` | `/${SafeSlug<infer _ extends string>}?${string}` | `/${SafeSlug<infer _ extends string>}#${string}` ? T : never) extends `/${SafeSlug<...>}` | ... 1 more ... | `/${SafeSlug<...>}#${string}` ? (`/${SafeSlug<...>}` | ... 1 more ... | `/${SafeSlug<...>}#$...'.ts(2322)

EDIT: If I remove the .next/types directory no error is shown.

Hello @karlhorky Maybe you could help to solve this typescript issue?

"next": "13.4.19",

{
  experimental: {
    serverActions: true,
    typedRoutes: true,
  },
 }
import { usePathname , useRouter } from "next/navigation";

const Comp = () => {
const router = useRouter();
const pathname = usePathname();


// Argument of type 'string' is not assignable to parameter of type 'RouteImpl<string>'.ts(2345)
 router.push(pathname)
}

Thank you!

Not ideal but this fixes the linter

router.push(url as Route)

Here’s another version, in case you want to have a nested data structure containing multiplle hrefs:

import Link, { LinkProps } from 'next/link';

type Props<RouteInferred> = {
  links: {
    title: string;
    href: LinkProps<RouteInferred>['href'];
  }[];
};

export default function Links<Route>(props: Props<Route>) {
  return (
    <div>
      {props.links.map((link) => {
        return <Link href={link.href}>{link.title}</Link>;
      })}
    </div>
  );
}

Or, using Route, a bit more verbose, but also works:

import { Route } from 'next';
import Link from 'next/link';

type Props<RouteInferred extends string> = {
  links: {
    title: string;
    href: Route<RouteInferred>;
  }[];
};

export default function Links<Route extends string>(props: Props<Route>) {
  return (
    <div>
      {props.links.map((link) => {
        return <Link href={link.href}>{link.title}</Link>;
      })}
    </div>
  );
}
myProp?: UrlObject | Route<T>

This was exactly what I needed — thx as always @shuding

Hello @karlhorky Maybe you could help to solve this typescript issue?

"next": "13.4.19",

{
  experimental: {
    serverActions: true,
    typedRoutes: true,
  },
 }
import { usePathname , useRouter } from "next/navigation";

const Comp = () => {
const router = useRouter();
const pathname = usePathname();


// Argument of type 'string' is not assignable to parameter of type 'RouteImpl<string>'.ts(2345)
 router.push(pathname)
}

Thank you!

or make the type inference work:

interface CommonButtonProps<T extends string> {
    buttonText: string;
    buttonType?: string;
    buttonGo?: UrlObject | Route<T>;
}

function CommonButton<T extends string>({ buttonText, buttonType, buttonGo }: CommonButtonProps<T>) {
    return (
        <div className="mt-8 text-center">
            {buttonType === 'Link' ? (
                <Link href={buttonGo} ...

A variation on this is what we ended up doing in our application.

Here’s our version, using the imported LinkProps, with more verbose generic argument / parameter names - this will cause CardLink[href] to be type checked the same way that Link[href] is:

import Link, { LinkProps } from 'next/link';

type CardLinkProps<RouteInferred> = LinkProps<RouteInferred>;

function CardLink<Route>(props: CardLinkProps<Route>) {
  return (
    <div className="m-3 inline-block">
      <Link href={props.href}>
        {props.children}
      </Link>
    </div>
  );
}

You can either do <Link href={buttonGo as Route}>

@shuding this type assertion currently fails with the @typescript-eslint/no-unnecessary-type-assertion rule (eg. if buttonGo is a string):

This assertion is unnecessary since it does not change the type of the expression. eslint(@typescript-eslint/no-unnecessary-type-assertion)
Screenshot 2023-09-13 at 19 08 03

This is a fairly new issue, since a few releases I think. Maybe I should create an issue.

As the docs says (link), you’ll have to either infer the correct link type, or cast it to Route so Next.js can statically check it.

In this example, you have buttonGo?: UrlObject | string and then pass it to <Link href={buttonGo}>. It means you are allowing any string to be the href of a link which is obviously a TS error:

interface CommonButtonProps {
    buttonText: string;
    buttonType?: string;
    buttonGo?: UrlObject | string;
}

const CommonButton: FC<CommonButtonProps> = ({ buttonText, buttonType, buttonGo }) => {
    return (
        <div className="mt-8 text-center">
            {buttonType === 'Link' ? (
                <Link href={buttonGo} className=" bg-green-600 hover:bg-green-500 text-white py-4 px-6 rounded-lg uppercase">
                    {buttonText}
                </Link>
            ) : (
                <button className="bg-green-600 hover:bg-green-500 text-white py-4 px-6 rounded-lg uppercase">
                    {buttonText}
                </button>
            )}
        </div>
    );
};

You can either do <Link href={buttonGo as Route}> to avoid the TS error or make the type inference work:

interface CommonButtonProps<T extends string> {
    buttonText: string;
    buttonType?: string;
    buttonGo?: UrlObject | Route<T>;
}

function CommonButton<T extends string>({ buttonText, buttonType, buttonGo }: CommonButtonProps<T>) {
    return (
        <div className="mt-8 text-center">
            {buttonType === 'Link' ? (
                <Link href={buttonGo} ...