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
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)
In file next.config.js, deactivate typedRoutes:
typedRoutes: false
The issue also applies to the official
app-dir-i18n-routingexample.Reproduction
typedRoutes: truetonext.config.jsapp/[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:
I get Errors on line 11 and 14 where strings are returned, but the Link component href is fine:
2. Typing of Link component
When I change line 8 and line 24:
I get a similar error to the original one (shorted version copied from VSCode tooltip):
EDIT: If I remove the
.next/typesdirectory no error is shown.Not ideal but this fixes the linter
Here’s another version, in case you want to have a nested data structure containing multiplle
hrefs:Or, using
Route, a bit more verbose, but also works: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",Thank you!
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 causeCardLink[href]to be type checked the same way thatLink[href]is:@shuding this type assertion currently fails with the
@typescript-eslint/no-unnecessary-type-assertionrule (eg. ifbuttonGois a string):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
Routeso Next.js can statically check it.In this example, you have
buttonGo?: UrlObject | stringand then pass it to<Link href={buttonGo}>. It means you are allowing any string to be thehrefof a link which is obviously a TS error:You can either do
<Link href={buttonGo as Route}>to avoid the TS error or make the type inference work: