0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Next.jsのDynamicRoutesに型をつける

Last updated at Posted at 2021-10-24

はじめに

TypeScriptでNext.jsを書くとき、
パスの生成やパスパラメータの取得に型が付くといいなと思っていたので、書きました。

リポジトリ

コード

import { useRouter } from "next/router";

const Paths = {
  home: "/",
  users: "/users",
  user: "/users/[userId]",
  blogs: "/users/[userId]/blogs",
  blog: "/users/[userId]/blogs/[blogId]",
  address: "/address/[prefecture]/[...cities]",
  calc: "/calc/[operator]/[[...numbers]]",
} as const;

type PathNames = keyof typeof Paths;
type Path = typeof Paths[PathNames];

type WithoutSlash<T> = T extends `/${infer U}` ? U : never;
type Resource<T> = T extends `${infer U}/${infer S}` ? U | Resource<S> : T;

type DynamicOptionalArrayRoute<T> = T extends `[[...${infer U}]]` ? U : never;
type DynamicArrayRoute<T> = T extends `[...${infer U}]` ? U : never;
type DynamicRoute<T> = T extends `[[...${infer _U}]]`
  ? never
  : T extends `[...${infer _U}]`
  ? never
  : T extends `[${infer U}]`
  ? U
  : never;

type OptionalArrayParams<T> = DynamicOptionalArrayRoute<
  Resource<WithoutSlash<T>>
>;
type ArrayParams<T> = DynamicArrayRoute<Resource<WithoutSlash<T>>>;
type Params<T> = DynamicRoute<Resource<WithoutSlash<T>>>;

type OptionalArrayParamKeys<T extends Path> = OptionalArrayParams<T>;
type ArrayParamKeys<T extends Path> = ArrayParams<T>;
type ParamKeys<T extends Path> = Params<T>;

type PathParams<T extends PathNames> = {
  pathname: T;
  params?: Partial<
    Record<OptionalArrayParamKeys<typeof Paths[T]>, (string | number)[]>
  > &
    Record<ArrayParamKeys<typeof Paths[T]>, (string | number)[]> &
    Record<ParamKeys<typeof Paths[T]>, string | number>;
};

type Args<T extends PathNames> = ParamKeys<typeof Paths[T]> extends never
  ? PathParams<T>
  : Required<PathParams<T>>;

export const createPath = <T extends PathNames>({
  pathname,
  params,
}: Args<T>) => {
  const path = Paths[pathname];

  if (params === undefined) {
    return path;
  }

  const directories = path.split("/");

  const replacedDirectories = directories.map((str) => {
    const matchOptionalArray = str.match(/\[\[\.\.\.(.*?)\]\]/);
    if (matchOptionalArray) {
      const key = matchOptionalArray[1] as ParamKeys<typeof path>;
      const param = params[key];
      return param ? param.join("/") : "";
    }

    const matchArray = str.match(/\[\.\.\.(.*?)\]/);
    if (matchArray) {
      const key = matchArray[1] as ParamKeys<typeof path>;
      return params[key].join("/");
    }

    const match = str.match(/\[(.*?)\]/);
    if (match) {
      const key = match[0];
      const trimmed = key.substring(1, key.length - 1) as ParamKeys<
        typeof path
      >;
      return params[trimmed];
    }

    return str;
  });

  return "/" + replacedDirectories.filter((d) => d !== "").join("/");
};

export const usePathParams = <
  T extends PathNames = PathNames,
  Query extends Record<string, string | string[]> = {}
>() => {
  const router = useRouter();
  const params = router.query as Partial<
    Record<OptionalArrayParamKeys<typeof Paths[T]>, string[]>
  > &
    Record<ArrayParamKeys<typeof Paths[T]>, string[]> &
    Record<ParamKeys<typeof Paths[T]>, string> &
    Partial<Query>;

  return params;
};

使い方

まず、パスを定数として定義したPathsを用意します。
keyにはページ名を、valueには動的ルーティングをstringで設定します。

パスの生成にはcreatePathを使用します。
pathnameを指定すると、必要なパラメータをparamsに渡すよう要求されます。

/**
 * 生成されるパス
 * /address/hokkaido/sendai/hakodate
 */
const address = createPath({
  pathname: "address",
  params: { prefecture: "hokkaido", cities: ["sendai", "hakodate"] },
});

各NextPageでパラメータを取得するときは、usePathParamsを使用します。
usePathParamsにPathsで定義したページ名を型引数として渡すことで、
対応したパラメータを取得できます。
他にクエリなども使用している場合は、型の第2引数にオブジェクトで渡します。

/**
 * prefecture: string;
 * cities: string[];
 * name?: string;
 */
const { prefecture, cities } = usePathParams<"address", { name: string }>();

さいごに

もっと良い書き方やバグ等あれば教えていただけると嬉しいです。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?