使用技術
- TypeScript
- React + Next.js も少々
ある日の我が家
娘(7歳)「うーん、TypeScriptのエラーが消えないなぁ・・・」
ワイ「お、どないしたんや?娘ちゃん」
娘「あのね?」
マッサージ店のサイトを作成している娘ちゃん
娘「お友達のおうちがマッサージ店をやっててね」
娘「そのお店のWebサイトを作ってあげてるの」
ワイ「おお〜、優しいな〜」
娘「月単価は150万だよ」
ワイ「ファッ!?」
ワイ「高っ」
ワイ「パパより単価高いやないかい」
ワイ「ぜんぜん優しくなかったわ」
娘「それでね」
娘「そのお店の名前が───」
「首・肩・肘」専門マッサージ店 ポキポキ
娘「───っていうんだけどね」
ワイ「なんや『ポキポキ』って」
ワイ「なんか折れる音してるやん」
ワイ「大丈夫かいなその店」
ワイ「そんで、何を悩んでたん?」
娘「あのね?」
料金一覧ページを作っていた
娘「マッサージ料金の一覧ページを作ってたの」
娘「料金はこんな感じ」
- 「首」コースの料金
- 500円プラン
- 50,000円プラン
- 「肩」コースの料金
- 500円プラン
- 50,000円プラン
- 「肘」コースの料金
- 500円プラン
- 50,000円プラン
ワイ「500円の次、いきなり50,000円かい」
ワイ「もっと中間のプランはないんかい」
ワイ「そんで、その料金一覧ページがどないしたん」
娘「あのね?」
タブ切り替え機能を作っていた
娘「↑こんな感じの、タブ切り替えのUIがあるの」
ワイ「なるほどな」
ユーザー「首のコースだけを絞り込んで表示したい!」
ワイ「↑こんなときに、タブを切り替えて表示できるんやな」
娘「そう」
娘「で、このタブUIを表示するために、あるオブジェクトを定義したの」
const courseNameMap: Record<Course, string> = {
kubi: "首",
kata: "肩",
hiji: "肘",
}
ワイ「ほうほう」
ワイ「このCourse
って型は何なん?」
娘「Course
型の中身は↓これだよ?」
type Course = "kubi" | "kata" | "hiji"
娘「これはopenapi.yaml
から自動生成した型なの」
ワイ「おお、ちゃんとopenapi.yaml
も書いてるんか」
娘「ううん、openapi.yaml
は」
娘「お友達のサーバサイド・エンジニアの手荒(てぃあら)ちゃんが書いてくれたの」
ワイ「手荒(てぃあら)ちゃん・・・?」
ワイ「えらい名前やな」
ワイ「とにかく、openapi.yaml
から自動生成したTypeScriptの型を使ってるんやな」
娘「そうそう」
娘「その型をオブジェクトのキーとして使ってあげれば、タイポの心配もないからね」
ワイ「なるほどな」
ワイ「Record<Course, string>
って部分やな」
娘「うん」
コースの分だけ、タブUIを表示する
娘「そして、さっきのcourseNameMap
っていうオブジェクトを使って」
娘「こんな感じでタブUIを実装してるの」
<ul>
{Object.entries(courseNameMap).map(([course, name]) => {
return (
<li key={course}>
<Link href={{ query: { course } }}>{ name }</Link>
</li>
)
})}
</ul>
ワイ「ほうほう」
ワイ「Object.entries()
を使って、オブジェクトを配列に変換して」
ワイ「配列のmap()
メソッドでループして」
ワイ「その分だけリンク・・・つまりタブを表示してやるんやな」
娘「そう、それで───」
- /price-list?course=kubi
- /price-list?course=kata
- /price-list?course=hiji
娘「↑こんな感じでURLパラメータをつけて、タブ切り替えを実装してるの」
ワイ「なるほどな」
ワイ「そんで、その"kubi"
やら"kata"
やらを」
ワイ「パラメータとしてサーバサイドのAPIに送ってやって」
ワイ「各コースの料金一覧データなんかをAPIから取得してくるわけやな」
娘「そんな感じ」
ワイ「ええやん」
ワイ「ほな、さっきは何を悩んでたの?」
「肩」または「肘」だけを受け入れる関数
娘「実はね」
娘「今、ポキポキさんでは0円キャンペーンっていうのを開催してて」
ワイ「ほうほう」
娘「そのキャンペーン用のメッセージを生成するための、ある関数を作ってたの」
娘「引数として"肩"
か"肘"
という文字列だけを受けとる関数なの」
const createMessage = (str: "肩" | "肘") => {
return `右の${str}だけなら0円でマッサージします!`
}
娘「そして、以下のようなメッセージを画面に表示するの」
ワイ「なんやそれ」
ワイ「右肩だけマッサージしてもらって何が嬉しいねん」
ワイ「左肩だけめっちゃ凝ってる状態、逆にイヤやわ」
娘「とにかく、引数として"肩"
か"肘"
という文字列だけを受け取る関数なの」
娘「なんだけど、実際にこの関数を実行しようとすると、TypeScriptのエラーが出ちゃうの」
エラー内容
型 'string' の引数を型 '"肩" | "肘"' のパラメーターに割り当てることはできません。
ワイ「へぇ〜」
娘「courseNameMap.kata
の中身は"肩"
っていう文字列だから」
娘「この関数に渡せるはずなのに!」
娘「"肩"
じゃなくてstring
型、っていう扱いになっちゃってるからダメみたい」
ワイ「なるほどな?」
ワイ「ほな、as
を使ってあげればエラーは消えるで」
<p>{ createMessage(courseNameMap.kata as "肩" | "肘") }</p>
ワイ「↑こうや」
娘「でも、それだと、TypeScriptのコンパイラを騙すことになっちゃうから」
娘「↓こういうこともできちゃうでしょ?」
{/* "首"を渡しても型エラーにならない */}
<p>{ createMessage(courseNameMap.kubi as "肩" | "肘") }</p>
娘「そうすると、画面にこんなことが表示されちゃうよ・・・!」
ワイ「Oh・・・」
ワイ「首が複数本ある感じになってしまうな」
娘「そうなの」
娘「そんな人間、3組の多首(おおくび)君くらいでしょ?」
ワイ「いや3組に存在すんのかい」
ワイ「とにかく、as
はアカンな・・・」
ワイ「ほな、as const
を使えばええんちゃうか?」
- const courseNameMap: Record<Course, string> = {
+ const courseNameMap = {
kubi: "首",
kata: "肩",
hiji: "肘",
- }
+ } as const
ワイ「↑こうや」
ワイ「こうしてやれば・・・」
ワイ「ほら、型エラーが消えたで」
娘「へぇ〜、as const
をつけると」
娘「courseNameMap.kata
が、string
じゃなくて"肩"
だって」
娘「具体的な型を推論してもらえるんだね!」
ワイ「せやで」
ワイ「具体的なリテラル型を推論してくれるんや」
娘「でもパパ」
娘「どうしてRecord<Course, string>
っていう型注釈を消しちゃったの?」
ワイ「as const
の恩恵を受けたい場合は、Record<Course, string>
っていう型注釈は併用できひんのや」
娘「そうなんだ・・・」
娘「まぁ、いっか!」
しかし、次の週・・・
ワイ「お、娘ちゃん」
ワイ「またポキポキさんのWebサイトをいじってるんか?」
娘「うん」
娘「実はね、お店の名前が微妙に変わったの!」
【旧】
「首・肩・肘」専門マッサージ店 ポキポキ
↓↓↓
【新】
「首・肩・肘・腰・膝・足首」専門マッサージ店 ポキポキ
娘「首・肩・肘だけじゃなくて」
娘「腰・膝・足首も専門的にマッサージしてくれるようになったの!」
ワイ「いや、もうそれ全身ですやん」
ワイ「どこが専門やねん」
ワイ「推しポイントが弱くなってもうてるで」
娘「とにかく、Webサイトの方にも」
娘「腰・膝・足首のコースの料金を表示しないといけないの」
娘「だから今、タブUIにも腰・膝・足首を追加するところなの」
娘「えっと・・・」
const courseNameMap = {
kubi: "首",
kata: "肩",
hiji: "肘",
+ koshi: "腰",
+ pizza: "膝",
+ ashikubi: "足首",
} as const
娘「↑こうだね!」
ワイ「娘ちゃん、アカンで!」
ワイ「hiza
がpizza
になってもうてるわ」
ワイ「ピッツァをマッサージしてどないすんねん」
ワイ「手がベチャベチャなってまうわ」
娘「うぅ・・・間違えた・・・」
娘「せっかく、キー名をタイポしないようにCourse
型を使っていたのに・・・」
娘「OpenAPIから生成した型を使っていたのに・・・」
娘「パパがRecord<Course, string>
っていう型注釈を消したから・・・」
ワイ「しゃ、しゃあないやろ」
ワイ「as const
を使って、具体的なリテラル型を推論させたかったんやから」
娘「as const
を使いつつ、オブジェクトのキーはCourse
型に縛りたい・・・」
娘「そんなことってできなかったっけ・・・?」
娘「・・・あっ!」
satisfies
を思い出した
娘「そういえば、TypeScript 4.9で追加された」
娘「satisfies
っていうのがあった!」
娘「あれを使えばいいんだ」
const courseNameMap = {
kubi: "首",
kata: "肩",
hiji: "肘",
koshi: "腰",
pizza: "膝",
ashikubi: "足首",
- } as const
+ } as const satisfies Record<Course, string>
娘「↑こうしてやれば・・・」
エラー内容
〜省略〜
'pizza' は型Record<Course, string>
に存在しません。
娘「ほら、Course
型の中に存在しないキー名を書いてしまった場合に」
娘「ちゃんと型エラーで気づくことができるの!」
娘「↑ちゃんと具体的な型推論もされてるよ!」
ワイ「おお〜」
ワイ「Course
型の制限も効かせつつ」
ワイ「as const
の恩恵も受けられるんやな」
娘「そうだね!」
ワイ「これは地味に便利やなぁ・・・!」
ワイ「ワイも勉強になったわ」
ワイ「ありがとうやで、娘ちゃん!」
その日の夜
ワイ「いや〜、TypeScriptってまだまだ進化してるんやなぁ」
娘「そうだね、地味に便利な機能がだんだん増えてくよね」
ワイ「それに比べて、ワイの能力は最近ほとんど進化してへんわ・・・」
ワイ「こんなんで、この先エンジニアとしてやって行けるんやろか・・・」
ワイ「若手エンジニアたちも、ワイなんかより全然優秀やし・・・」
ワイ「最近はChatGPTに職を奪われる心配もあるし・・・」
ワイ「不安や・・・不安や・・・」
ワイ「いや、アカン」
ワイ「あんまり不安を感じないようにせんと・・・」
ワイ「ポジティブ!ポジティブや!」
娘「不安でいいんだよ」
ワイ「えっ・・・」
娘「不安だって、必要な感情だから脳が与えてくれてるんだよ」
娘「失敗しないために。悪い未来を避けるために」
ワイ「そうか・・・」
娘「この先も、ちゃんとエンジニアとして活躍したい」
娘「そんな気持ちの裏返しだからね」
娘「全否定することじゃないんだよ」
ワイ「確かにな」
ワイ「不安はいつも、ワイを翻弄してくる存在やと思ってたけど」
ワイ「ワイを導いてくれる存在でもあるのかもな」
娘「うん」
ワイ「そう考えたら、少し楽になったわ」
ワイ「ありがとうやで、娘ちゃん・・・」
〜おしまい〜