---
title: 型パズルでページ内リンクのtypoを根絶してみた話
tags: 
author: [堀池浩輝](https://www.docswell.com/user/h_horiike)
site: [Docswell](https://www.docswell.com/)
thumbnail: https://bcdn.docswell.com/page/KJ4286VYE1.jpg?width=480
description: Shizuoka.js #7 登壇資料
published: October 21, 23
canonical: https://www.docswell.com/s/h_horiike/Z1J4N4-shizuokajs7
---
# Page. 1

![Page Image](https://bcdn.docswell.com/page/KJ4286VYE1.jpg)

型パズルで
typoを
ページ内リンクの
根絶してみた話
堀池 浩輝 @tender_ice


# Page. 2

![Page Image](https://bcdn.docswell.com/page/LE1M2V3N7G.jpg)

自己紹介
SBS情報システム
堀池 浩輝
@tender_ice (リア垢)
静岡新聞SBSグループのIT企業、県内SIer
Webアプリケーションが主で TypeScript が好き
静岡県が設置したイノベーション施設 SHIP のコアメンバー
技術アカウントもあるけどネット転生予定なので今回は伏せます


# Page. 3

![Page Image](https://bcdn.docswell.com/page/GEW3RLXME2.jpg)

ページ内リンクの
typoを
根絶したい！


# Page. 4

![Page Image](https://bcdn.docswell.com/page/47ZVRM6ME3.jpg)

たとえば React Router の場合
&lt;BrowserRouter&gt;
&lt;Routes&gt;
&lt;Route index element={&lt;TopPage /&gt;} /&gt;
&lt;Route path=&quot;/signin&quot; element={&lt;SignIn /&gt;} /&gt;
&lt;Route path=&quot;/items&quot; element={&lt;ItemListPage /&gt;} /&gt;
&lt;Route path=&quot;/items/:itemId&quot; element={&lt;ItemDetailsPage /&gt;} /&gt;
&lt;Route path=&quot;/options/:tabId&quot; element={&lt;Options/&gt;} /&gt;
･
･
･
{/* Nested Routing は使わない想定 */}


# Page. 5

![Page Image](https://bcdn.docswell.com/page/YJ65RN257V.jpg)

typoがつらい
&lt;Link to=&quot;/item&quot;&gt;Products&lt;/Link&gt;
あれ……「/item」だっけ？「/items」だっけ？
&lt;Link to=&quot;/options&quot;&gt;設定&lt;/Link&gt;
あれ……「/options」だけじゃダメなんだっけ？
さらに、だんだん階層が深くなってくると悲惨
&lt;Link to=&quot;/shop/sId123/items/iId123/comments/cId123&quot;&gt;…&lt;/Link&gt;


# Page. 6

![Page Image](https://bcdn.docswell.com/page/GJ5Y852GJ4.jpg)

あれ…なんで俺…
定義済みの値を
手打ちしてんだ…？


# Page. 7

![Page Image](https://bcdn.docswell.com/page/9E2GKN4DER.jpg)

必須のIDに型推論が効く関数が欲しい
getItemListPagePath() // =&gt;`/items`
getItemDetailsPagePath() // 引数が1つ必要です
getItemDetailsPagePath(itemId) // =&gt;`/items/${itemId}`
つまり、
getItemListPagePath = () =&gt; `/items`
getItemDetailsPagePath = (itemId) =&gt; `/items/${itemId}`


# Page. 8

![Page Image](https://bcdn.docswell.com/page/D7YVNPMM7M.jpg)

二重管理もつらい
const pagePaths = {
signin: () =&gt; &quot;/signin&quot;,
itemList: () =&gt; &quot;/items&quot;,
itemDetails: (itemId:string)=&gt;`/items/${itemId}`,
options: (tabId:string)=&gt;`/items/${tabId}`,
}
画面を追加するたびに
こっちにも追加するのを忘れる
片方だけ変更してしまってバグった


# Page. 9

![Page Image](https://bcdn.docswell.com/page/VENMX5W3J8.jpg)

1つのオブジェクトから作ろう！
const pagePaths = {
signin: { value: &quot;/signin&quot;, get: () =&gt; &quot;/signin&quot; },
itemList: { value: &quot;/items&quot;, get: () =&gt; &quot;/items&quot; },
itemDetails: { value: &quot;/items/:itemId&quot;, get: (itemId:string) =&gt; `/items/${itemId}` },
options: { value: &quot;/options/:tabId&quot;, get: (tabId:string) =&gt; `/options/${tabId}` },
};
ルーティングを定義するときも、&lt;Link&gt;を使うときも
pagePathsオブジェクトから参照すればOKにしよう！


# Page. 10

![Page Image](https://bcdn.docswell.com/page/Y79N8YXP73.jpg)

プログラマの美徳「怠惰」発動
itemDetails: { value: &quot;/items/:itemId&quot;, get: (itemId:string) =&gt; `/items/${itemId}`
Pathが items/:itemId って言ってるんだから
itemDetails.get が (itemId:string) =&gt; `items/${itemId}`
になるなのは自明じゃね…？
自明なことはコンピュータにやらせたい！書きたくない！めんどくさい！
その手作業でミスがあったらどうするんだ！


# Page. 11

![Page Image](https://bcdn.docswell.com/page/G78K862XJD.jpg)

“item/:itemId” を渡したら
(itemId:string) =&gt; `item/${itemId}`
自動で作って型推論も効く
も
createPagePath() 的な関数が欲しい！


# Page. 12

![Page Image](https://bcdn.docswell.com/page/L7LZR92NER.jpg)

みんな だいすき 型 パズル
type DynamicPaths&lt;
T extends `/${string}`,
P extends string[] = []
&gt; = T extends `/:${string}/${infer R}`
? DynamicPaths&lt;`/${R}`, [...P, string]&gt;
: T extends `/${string}/${infer R}`
? DynamicPaths&lt;`/${R}`, [...P]&gt;
: T extends `/:${string}`
? [...P, string]
: [...P];
/foo/bar ⇒ []
/foo/:bar ⇒ [string]
/:foo/:bar ⇒ [string, string]
DynamicPaths&lt;T,P&gt; は Tuple で表現される string の固定長配列であり、
その長さは T に含まれる /: の数に等しい。( P は再帰でのみ使用される)


# Page. 13

![Page Image](https://bcdn.docswell.com/page/4EMVRZ8QEW.jpg)

完成した createPagePath()
const createPagePath = &lt;T extends `/${string}`&gt;(path: T) =&gt; {
return {
value: path,
get(...args: DynamicPaths&lt;T&gt;) {
const iter = args.entries();
return path
.split(&quot;/&quot;)
.map(seg =&gt; seg.match(/^:.+/) ? iter.next().value[1] : seg)
.join(&quot;/&quot;);
},
};
};


# Page. 14

![Page Image](https://bcdn.docswell.com/page/PER2R65K79.jpg)

使ってみる


# Page. 15

![Page Image](https://bcdn.docswell.com/page/P7XDRMK5JX.jpg)

教訓と感想
●
Variadic Tuple Types と再帰呼び出しの相性が良い
●
Template Literal Types と infer をうまく使うと文字列操作が捗る
●
今回の物だと Nested Routing に対応できないので、ライブラリ側でうまい
こと Route を定義したら getPagePaths() 関数を提供して欲しい…（切実）
●
こういうハックは楽しいが、やりすぎ注意（戒め）


# Page. 16

![Page Image](https://bcdn.docswell.com/page/37KXRG5RJD.jpg)

ごあんない
「私の作品が映画館で流れる！？」
あなたが AI で作った動画
あなたが 360°視点映像 で撮った動画
映画館で流しませんか？
【映像クリエイティブチャレンジ2023】開催！
興味のある方は、
SHIPの関連セミナーに是非ご参加ください！


# Page. 17

![Page Image](https://bcdn.docswell.com/page/LJ358NKG75.jpg)

型パズルで
typoを
ページ内リンクの
根絶してみた話
ご清聴ありがとうございました


