型パズルでページ内リンクのtypoを根絶してみた話

1.1K Views

October 21, 23

スライド概要

Shizuoka.js #7 登壇資料

profile-image

SBS情報システム エンジニア/企画・開発・研究・教育・デザイン/TypeScript

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

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

2.

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

3.

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

4.

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

5.

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

6.

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

7.

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

8.

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

9.
[beta]
1つのオブジェクトから作ろう!
const pagePaths = {
signin: { value: "/signin", get: () => "/signin" },
itemList: { value: "/items", get: () => "/items" },
itemDetails: { value: "/items/:itemId", get: (itemId:string) => `/items/${itemId}` },
options: { value: "/options/:tabId", get: (tabId:string) => `/options/${tabId}` },
};

ルーティングを定義するときも、<Link>を使うときも
pagePathsオブジェクトから参照すればOKにしよう!

10.
[beta]
プログラマの美徳「怠惰」発動
itemDetails: { value: "/items/:itemId", get: (itemId:string) => `/items/${itemId}`

Pathが items/:itemId って言ってるんだから
itemDetails.get が (itemId:string) => `items/${itemId}`
になるなのは自明じゃね…?
自明なことはコンピュータにやらせたい!書きたくない!めんどくさい!
その手作業でミスがあったらどうするんだ!

11.

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

12.
[beta]
みんな だいすき 型 パズル
type DynamicPaths<
T extends `/${string}`,
P extends string[] = []
> = T extends `/:${string}/${infer R}`
? DynamicPaths<`/${R}`, [...P, string]>
: T extends `/${string}/${infer R}`
? DynamicPaths<`/${R}`, [...P]>
: T extends `/:${string}`
? [...P, string]
: [...P];

/foo/bar ⇒ []
/foo/:bar ⇒ [string]
/:foo/:bar ⇒ [string, string]

DynamicPaths<T,P> は Tuple で表現される string の固定長配列であり、
その長さは T に含まれる /: の数に等しい。( P は再帰でのみ使用される)

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

14.

使ってみる

15.

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

16.

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

17.

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