使用技術
- 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に職を奪われる心配もあるし・・・」
ワイ「不安や・・・不安や・・・」
ワイ「いや、アカン」
ワイ「あんまり不安を感じないようにせんと・・・」
ワイ「ポジティブ!ポジティブや!」
娘「不安でいいんだよ」
ワイ「えっ・・・」
娘「不安だって、必要な感情だから脳が与えてくれてるんだよ」
娘「失敗しないために。悪い未来を避けるために」
ワイ「そうか・・・」
娘「この先も、ちゃんとエンジニアとして活躍したい」
娘「そんな気持ちの裏返しだからね」
娘「全否定することじゃないんだよ」
ワイ「確かにな」
ワイ「不安はいつも、ワイを翻弄してくる存在やと思ってたけど」
ワイ「ワイを導いてくれる存在でもあるのかもな」
娘「うん」
ワイ「そう考えたら、少し楽になったわ」
ワイ「ありがとうやで、娘ちゃん・・・」
〜おしまい〜









