Next.jsのDynamicRoutesに型をつける

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

# リポジトリ
https://github.com/alternacrow/next-routing-with-typescript

# コード
```typescript
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に渡すよう要求されます。

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

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

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

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

# 参考
https://zenn.dev/panda_program/articles/typescript-nextjs-routing