tatethurston / nextjs-routes Goto Github PK
View Code? Open in Web Editor NEWType safe routing for Next.js
License: MIT License
Type safe routing for Next.js
License: MIT License
My pages
folder is in the project root. The generated types file looks like this:
export type Route =
| { pathname: "pages/_app"; query?: Query | undefined }
| { pathname: "pages/_document"; query?: Query | undefined }
| { pathname: "pages/_error"; query?: Query | undefined }
hence, I get type errors as the correct paths don't have the pages/
prefix.
Apologies for multiple issues this week. Trying to type all the things with the new next13.
I notice that useRouter
is typed only from the next/link
import and not the new next/navigation
module, and I was wondering if it will be supported in the future.
Thanks for the great package!
Hi,
I am using the [email protected] with the withRoutes
configuration but I am not sure it's working as expected.
I am seeing the following error
I also cannot see the nextjs-routes nextjs-routes.d.ts types or any addition to my tsconfig.json.
Any ideas what am I missing?
If a page is statically optimized, meaning it is pre-rendered on the server, the router's query object will be empty when the component first renders (hydration pass). Only after hydration, Next.js triggers an update to provide the route parameters in the query object.
As a consequence, the following query object should be typed as { foo: string | undefined }
as opposed to { foo: string }
.
import { useRouter } from "next/router";
// query is now typed as `{ foo: string }`
const { query } = useRouter<"/foos/[foo]">();
Next.js does this because React expects that there is no difference between the React tree that was pre-rendered and the React tree that was rendered during the first render (hydration) in the Browser. See React docs for more.
If a page uses getServerSideProps
and getInitialProps
, the query object won't be empty in the first render. See Automatic Static Optimization for more on this.
The router provides the value isReady
to check if the router fields have been updated client-side and are ready for use. So ideally, checking for isReady
should type narrow the query values:
import { useRouter } from "next/router";
const { query, isReady } = useRouter<"/foos/[foo]">();
const { foo } = query;
// `foo` is `string | undefined`
if (isReady) {
// `foo` is now `string`
functionThatExpectsStringParamater(foo);
}
I have reproduced the type error in this repository: https://github.com/IGassmann/query-type-error
If you open the file pages/[name].tsx
, you won't see any type error for the function call expectStringParameter(name);
, however, when you run the page, you will get a runtime error for the type of name
. This happens because the name
query value is incorrectly typed by nextjs-routes
.
Hi,
I tried to change the locale and stay on the same route by following nextjs exemple.
const router = useRouter()
const { pathname, asPath, query } = router
// change just the locale and maintain all other route information including href's query
router.push({ pathname, query }, asPath, { locale: nextLocale })
Unfortunately I have a typescript error with nextjs-routes.
I fixed it with an assertion any but maybe you have a better solution?
Thanks for your help!
Remove cli invocation of nextjs-routes: eg npx nextjs-routes
and yarn nextjs-routes
.
Direct users to use automatic wiring instead:
+ const { withRoutes } = require("nextjs-routes/next-config.cjs");
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
};
- module.exports = nextConfig;
+ module.exports = withRoutes(nextConfig);
This enables richer configuration in the future, leveraging withRoutes
to supply any configuration options.
A deprecation notice will log for usage of npx nextjs-routes
and yarn nextjs-routes
, directing to this issue. Any objects to this change can be raised on this issue. After a period of time if no persuasive objections are raised, this usage will removed from the package.
The standard type for locale
, for example in const { locale } = useRouter()
is always string|undefined
.
undefined
is only applicable, if i18n is not configured.
What do you mean, is it in the scope of the package that locale
and locales
are typed properly related to the configuration in the next.config.js?
A config which looks like this...
{
module.exports = {
i18n: {
localeDetection: true,
defaultLocale: 'de-DE',
locales: ['de-AT', 'de-DE', 'de-CH', 'en-FR', 'en-IT', 'en-GB', 'en-US']
}
}
... would result into a type override which looks like this:
declare module 'next/router' {
import type { NextRouter as NextBaseRouter } from 'next/dist/client/router'
export * from 'next/router'
export { default } from 'next/router'
type NextRouter = Omit<NextBaseRouter, 'locale'> & {
locale: ['de-AT', 'de-DE', 'de-CH', 'en-FR', 'en-IT', 'en-GB', 'en-US']
}
export type { NextRouter }
export function useRouter(): NextRouter
}
This is just a quick draft. We need to dig deeper into the package as, unfortunately, locale
is typed on many code pieces in the routing types.
Looking forward for feedback about the idea. Maybe an v1 feature as well...
Why is the query parameter for a page route typed as string | string[] | undefined
? Shouldn't a matched route always be available and a single string?
Great lib and great work on that!
I'm using the nextjs-routes v1.0.4 and in the README example :
import { useRouter } from "next/link";
const router = useRouter();
router.push({ pathname: "/foos/[foo]", query: { foo: "test" } });
from "next/router"
pathname
from router.push
is not available but query
does.Can you confirm @tatethurston ?
I am using Next.js with nextjs-routes together with Mantine. Mantine has a component Menu.Item
where all its props are inferred by a component
prop. Unfortunately, this fails when setting component={Link}
as it always infers that the href
of Link
is only of type Query
(and doesn't consider Route
and StaticRoute
).
Currently, Link
is defined this way:
declare function Link(
props: PropsWithChildren<LinkProps<Route>>
): LinkReactElement;
declare function Link(
props: PropsWithChildren<LinkProps<StaticRoute>>
): LinkReactElement;
declare function Link(
props: PropsWithChildren<LinkProps<Query>>
): LinkReactElement;
export default Link;
But the issue would be fixed if it would be defined this way:
declare function Link(
props: PropsWithChildren<LinkProps<Route | StaticRoute | Query>>
): LinkReactElement;
export default Link;
I can't think of any drawbacks, but I am not a TS expert.
We are using redirects for cases when data is not found or not present. Any way here to also type the source and destination parts for the redirect return?
For example
export const getServerSideProps: GetServerSideProps<DetailsPageProps> = async (context) => {
const result = some fetching
const data = result.data;
if (!data) {
return {
redirect: {
source: '/data/[id]',
destination: '/',
permanent: false,
},
};
}
return {
props: {
data,
},
};
};
Hi,
First things first, thank you for this great tool! Amazing and accurate work.
I'm facing a dependency resolution issue that's keeping me from building the project without having to comment / disable the nextjs-routes require line.
Important to note that although the module is not resolved and ESLinting showing an error, it still works perfectly and generates routes at the right location as expected.
Any clue?
Thanks!
Hi!
I was glad to find this module, and at first, it worked pretty well. But when I started to build our code, which is a part of a bigger thing, I ran into a problem with that pathname
is too strict and doesn't allow other strings, which may occur outside of the project src files.
E.g. this:
...(router.pathname === "/dashboard" && ...
makes an error:
Type error: This condition will always return 'false' since the types '"/404" | "/[id]" | "/" | ... and '"/dashboard"' have no overlap.
I can only edit the generated file now like this, to allow for string checks:
export interface NextRouter<P extends Pathname = Pathname>
extends Omit<Router, "push" | "replace"> {
pathname: P | string; /// <<<<<< ` | string` is added HERE
route: P;
query: QueryForPathname[P];
...
}
but it's gonna be rewritten.
I'd propose to use an overridable template instead. What do you think?
Currently, these won't type properly if you wanted the equivalent of href="#some-id"
const id = "some-id"
<Link
href={`${id}`}
/>
or
<Link
href={{
hash: id
}}
/>
Should just need to union a hash only type:
type Hash = { hash: string }
export interface LinkProps ... {
// Union `Hash` here
href: Route | StaticRoute | Query | Hash
...
}
The type checking is less precise as it does not tell me if I have mistyped the path parameter because it could also be any query parameter.
Next.js 13 is now out and it released the new layout system under the new app
folder.
Is it something this package is interested in adopting?
The route structure is slightly different in that folder and both
app
andpages
can be used at the same time so the tool would have to be able to merge the two route structures.
Love the type-safe routes!
However, I am having issues using the features nextjs-routes makes available with Catch All Routes:
https://nextjs.org/docs/api-routes/dynamic-api-routes#catch-all-api-routes
This comes around due to some SDKs requiring catch alls or optional catch alls:
https://github.com/auth0/nextjs-auth0#add-the-dynamic-api-route
Probably not an easy one to fix, but would be really powerful to be able to handle catch-all routes.
Since Next allows href
to be a string in Link
s, it would be nice to also offer this more compact syntax (e.g.: Allow users to write href="/"
instead of href={{ pathname: '/' }}
). We can still make this typed so that only the corresponding string literals are allowed.
If this sounds like a good idea, I can create the PR with the implementation :)
If I have an route which catches all subpaths such as "/api/as-api/[...as-proxy]"
the generated route is { pathname: "/api/as-api/[...as-proxy]"; query: Query<{ as-proxy: string[] }> }
. Notice that there is a key, as-proxy
, which is missing quotes to allow it to be a valid object key.
Using v0.0.18
It seems that only next dev generates the file, but next build does not, so if I .gitignore the generated typings, build fails on CI.
I read in the readme that the route typings are generated during build also, but cannot reproduce this.
next build
Since typings are not created, the typescript build fails if we
import type { Route } from "nextjs-routes";
The file is also generated during build
I am using:
I have the following file:
pages/automator-configs/[label].tsx
:
import { GetServerSideProps, InferGetServerSidePropsType } from 'next/types';
import { getAutomatorConfig } from '../../ansibleAdapter/automatorConfig';
import type { RoutedQuery } from 'nextjs-routes';
export const getServerSideProps: GetServerSideProps<{}, RoutedQuery<'/automator-configs/[label]'>> = async (
context,
) => {
const { label } = context.params;
return { props: { automatorConfig: getAutomatorConfig()[label] } };
};
export default function AutomatorConfig({ automatorConfig }: InferGetServerSidePropsType<typeof getServerSideProps>) {
return <pre className="max-h-96">{JSON.stringify(automatorConfig, null, 2)}</pre>;
}
However, this shows several eslint and TypeScript errors:
For the first generic in GetServerSideProps
, eslint shows: “Don't use {}
as a type. {}
actually means "any non-nullish value"”. I guess this can be ignored for now.
context.params
is resolved as:
(property) params?: ({
label: string;
} & Query) | undefined
so I cannot do const { label } = context.params;
, because it shows:
Property 'label' does not exist on type '({ label: string; } & Query) | undefined'.ts(2339)
In my nextjs-routes.d.ts
, I see:
declare module "nextjs-routes" {
export type Route =
| StaticRoute<"/404">
| DynamicRoute<"/automator-configs/[label]", { "label": string }>
| StaticRoute<"/">;
interface StaticRoute<Pathname> {
pathname: Pathname;
query?: Query | undefined;
hash?: string | null | undefined;
}
interface DynamicRoute<Pathname, Parameters> {
pathname: Pathname;
query: Parameters & Query;
hash?: string | null | undefined;
}
interface Query {
[key: string]: string | string[] | undefined;
};
export type RoutedQuery<P extends Route["pathname"]> = Extract<
Route,
{ pathname: P }
>["query"];
export type Locale = undefined;
/**
* A typesafe utility function for generating paths in your application.
*
* route({ pathname: "/foos/[foo]", query: { foo: "bar" }}) will produce "/foos/bar".
*/
export declare function route(r: Route): string;
}
So it makes sense that the query would also possibly be undefined
. However, this is not what's shown in the docs.
What would be the recommended way to solve this issue?
This is more of something that would take nextjs-routes to the next level for us, but it's already a great solution. It would be nice if we could augment the query
types for each route to add our own. For example, defining a query param called edit
on the /user/[id]
route to change the page to edit mode.
This is just an feature request right now, I'll try to think of some ideas on how we could do this later.
because it is assumed that the plugin will find the app
or pages
directory relative to process.cwd()
, this plugin doesn't work in a nxdev monorepo.
The solution is to:
edit:
cwd
for dir
(or update the code to talk about cwd
)dir
(edit: solved with outdir
)Hi,
We're using this library and it's great for mapping the slug parts of a path to their required values or query parameters. However we also using actual query string values for certain items and it appears as though that isn't possible here without the types throwing an error.
Any recommended way to do something like this?
<Link
href={{
pathname: '/api//foo/[id]',
query: {
id: 'bar',
mode: 'baz', // this complains
},
}}
>
<a>A Thing</a>
</Link>
To me it would be nice to change the query
value to params
and then just let people merge in their own query parameters in an un-type-safe kind of way. Otherwise people are prevented from making use of some pretty important browser functionality.
Now that the API has begun to stabilize, we should consider what (if anything) should be added before releasing a v1. That release will mark the adoption of semantic versioning, to make it easier for users to reason about updates.
For me, the generated output has ;
s instead of ,
s to separate object entries which is incorrect and errors.
export type Route =
| { pathname: "/api/dataset/[datasetId]/[datasetVersion]"; query: Query<{ datasetId: string; datasetVersion: string }> }
| { pathname: "/api/project/[projectId]/file"; query: Query<{ projectId: string }> }
…
using v0.0.18
Hello 🙂! This package supports 'rewrites'?
If don't, there are reasons (or issues) to support it?
I'm trying to understand how nextjs-route can handle ?query=part
?
For example, we have a page with a table that is supposed to be paged. So I need to pass count and page for every URL.
But nextjs-routes Router doesn't know about our params and I get errors.
You have implemented a good module which can be used for more potential use cases.
I personally would like to have it for making custom navigation that will be auto generated from available routes.
I see that you can have an can pass a callback option to NextJSRoutesPlugin.
This callback will accept parsed data from routes and will do code generation.
class NextJSRoutesPlugin implements WebpackPluginInstance {
apply() {
// ...
if (this.context.dev) {
const watcher = watch(watchDirs, {
persistent: true,
});
// batch changes
const generate = debounce(() => this.processRoutes, 50);
watcher.on("add", generate).on("unlink", generate);
} else {
this.processRoutes();
}
}
collectRoutes() {
const defaultOptions = {
// ...
};
const opts = {
...defaultOptions,
...this.options,
};
// ...
return nextRoutes(files);
}
processRoutes: () => {
this.options.callback(this.collectRoutes)
}
}
config.plugins.push(
new NextJSRoutesPlugin(nextConfig, context, {
...options,
callback: (routes) => {
const generated = generate(routes, options);
writeFileSync(outputFilepath, generated);
}
})
);
Hey, thanks for the amazing project! In my app I use the src/pages
directory but it doesn't work for nextjs-routes
because it only looks in the pages
directory.
Next's documentation notes the following:
During prerendering, the router's query object will be empty since we do not have query information to provide during this phase. After hydration, Next.js will trigger an update to your application to provide the route parameters in the query object.
To ensure type safety, nextjs-routes assumes every page is optimized. This requires that clients always use router.isReady
to narrow the router.query
type when using useRouter
.
If we import
/require
the transpiled page files (or read the AST), we can detect whether a page will be optimized. This will also unblock #132 and possibly #39.
A few considerations:
There will be some tradeoffs here wrt complexity, we may need to investigate a few different solutions to minimize the amount of complexity this package takes on.
I wanted to use this in a non-link. For example a fetch like fetch('/foo')
, is there anyway to typecheck that foo?
I was thinking of doing a type like:
const ResourcePath = Parameters<typeof useRouter>[0]['path'];
const path: ResourcePath = '/foo';
fetch(path);
But it's kind of extra to create a variable to hold path then type that.
nextjs-routes can be invoked via cli: npx nextjs-routes
. Presently, no configuration options are passed to the cli execution.
nextjs-routes.config.js
if present.writeNextjsRoutes
in src/cli
.Hi there,
Using v0.1.0, I noticed that it doesn't seem to support Custom Page Extensions. Indeed, the generated nextjs-routes.d.ts
file defines a route for every files found in the pages
(or src/pages
) directory, including non page ones.
I think it might be something you want to consider for #44.
Feel free to ask me anything that can help.
Thanks for this tool, Vercel should take inspiration from it!
First, If you are considering this, I am sorry for registering the issue.
When I run the examples/app folder(nextjs version is 13.3.0), I get the following error.
If you go to the link provided above, next.js says that url mapping is no longer available for the app directory structure.
[example code]
<Link href={{ pathname: "/[store]", query: { store: "tate" } }}>
Tate's Store
</Link>
I think we need to figure out a way to use it in the app directory of Next.js@13.
If there's anything I made a mistake, please give me some advice!
For dynamic routes that export getStaticPaths
it would be useful to have a config setting to enable automatic translation from DynamicRoute
-> StaticRoute
.
You definitely want this off by default in case it's an expensive operation
Give the page/pathname:
"/blog/[slug]"
which exports getStaticPaths
:
const getStaticPaths = () => ({
paths: ["1", "2", "3"]
})
Now, instead of just:
<DynamicRoute<"/blog/[slug]", { "slug": string }>
You get:
<DynamicRoute<"/blog/[slug]", { "slug": string }>
<StaticRoute<"/blog/1">
<StaticRoute<"/blog/2">
<StaticRoute<"/blog/3">
One small detail is that if you have fallback: false
const getStaticPaths = () => ({
paths: ["1", "2", "3"],
fallback: false
})
The DynamicRoute
should be omitted:
<StaticRoute<"/blog/1">
<StaticRoute<"/blog/2">
<StaticRoute<"/blog/3">
Hello,
I have Nextjs 13.1.6 and Jest for building my app.
but while testing I use function route()
from nextjs-routes
it cause error and make test suite failed.
this is some sample error I got
/node_modules/.pnpm/[email protected][email protected]/node_modules/nextjs-routes/index.js:1
({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest){export function route(r) {
^^^^^^
SyntaxError: Unexpected token 'export'
How I can solve this problem thank you :)
I'd like to be able to manually generate the nextjs-routes.d.ts
file instead of relying on the file watcher on the nextjs dev server. The main reason for this is so we can manually generate types in CI, but we also have some nextjs config which appears to break when we use withRoutes()
in next.config.js
. Additionally, we have other codegen in our repo, so we'd like to be able to have one script that does all of it at once.
What I'd like to do is to call the function writeNextjsRoutes
manually in a script, but it's not exported so I can't use it. It would be even better if I could run a command like yarn nextjs-routes codegen
to generate types, but I'll settle for the easiest possible soution.
Thanks! Love this project. I'm excited to be able to use it.
In nextj 13, NextLink
accepts anchor props. However, adding className to NextLink after installing nextjs-routes
results in a type error. that className
is not an acceptable prop.
next-themes v 1.0.1
node 18
next 13.0.4
Hi there,
As I build up my codebase, I came on the use case of having a link with a hash to open a page scrolled on a specific id. While the original next/link
component supports it (with the help of the scroll
prop), the routes type definition generated by nextjs-routes
prevents it as there is no hash possible in pathname
.
I think this could be addressed with template literal string, but I wonder what do you think of it?
When using Next.js 13, is there a setting to enable the use of path strings when configured under the app folder?
I am getting this error when I use my custom Link component Type 'string' is not assignable to type 'Route | Query'
but It works with default Link component from next js. I am doing something wrong?
this is my custom Link component
import NextLink, { LinkProps as NextLinkProps } from 'next/link'
export type LinkProps = Omit<NextLinkProps, 'passHref' | 'as'> & Omit<ChakraLinkProps, 'href'>
export const Link: React.FC<React.PropsWithChildren<LinkProps>> = ({
children,
href,
onClick,
className,
replace,
scroll,
shallow,
prefetch,
locale,
...props
}) => {
return (
<NextLink href={href} legacyBehavior passHref>
<ChakraLink {...props}>{children}</ChakraLink>
</NextLink>
)
}
When I use it like this I will get error I mentioned above but when I import Link from next js everything works as expected.
<Link href="/users">text</Link>
See: https://github.com/tatethurston/nextjs-routes/blob/main/src/cli.ts#L25
It should pull in the local config, like it does on the webpack hook so the i18n
config gets properly typed
I've been getting weird false/positives locally since adding:
// package.json
{
"scripts": {
"prebuild": "npx nextjs-routes"
}
}
This so cool. I will use it, but can you export it to an interface too? Sometimes need use like this
const myText=`this is a link ${ROUTES.ITEM({itemId:"abcdefg"})} `
Originally posted by @IRediTOTO in vercel/next.js#28622 (reply in thread)
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.