vercel / vercel/next.js

Statically Typed Links not working when passing href

Open
#47,689 33 comments 17 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Linking and Navigating TypeScript
Dominant language
JavaScript
Stars
142k
Forks
32.4k
Avg merge
2d 14h
Merged PRs (30d)
351

Description

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=%5B%7B%22endColumn%22%3A17%2C%22endLineNumber%22%3A32%2C%22startColumn%22%3A17%2C%22startLineNumber%22%3A32%7D%5D

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce the report from the linked CodeSandbox and run tsc --noEmit with TypeScript 5.0.2. Inspect the generated .next/types/link.d.ts, especially the Link href declaration, alongside the Pager callback type. Done means a callback returning Route | URL can be passed to Link without the reported type errors.

Written by the indexing model from the issue text.

Assessment

Tech stack
nextjs, react, typescript
Domain
frontend, web-dev
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.