LoginSignup
136
70
お題は不問!Qiita Engineer Festa 2023で記事投稿!

7歳娘「パパ、TypeScriptのsatisfiesって便利だね!」

Last updated at Posted at 2023-06-28

使用技術

  • TypeScript
  • React + Next.js も少々

ある日の我が家

娘(7歳)「うーん、TypeScriptのエラーが消えないなぁ・・・」

ワイ「お、どないしたんや?娘ちゃん」

娘「あのね?」

マッサージ店のサイトを作成している娘ちゃん

娘「お友達のおうちがマッサージ店をやっててね」
娘「そのお店のWebサイトを作ってあげてるの」

ワイ「おお〜、優しいな〜」

娘「月単価は150万だよ」

ワイ「ファッ!?
ワイ「高っ
ワイ「パパより単価高いやないかい」
ワイ「ぜんぜん優しくなかったわ」

娘「それでね」
娘「そのお店の名前が───」

「首・肩・肘」専門マッサージ店 ポキポキ

娘「───っていうんだけどね」

ワイ「なんや『ポキポキ』って」
ワイ「なんか折れる音してるやん」
ワイ「大丈夫かいなその店」
ワイ「そんで、何を悩んでたん?」

娘「あのね?」

料金一覧ページを作っていた

娘「マッサージ料金の一覧ページを作ってたの」
娘「料金はこんな感じ」

  • 「首」コースの料金
    • 500円プラン
    • 50,000円プラン
  • 「肩」コースの料金
    • 500円プラン
    • 50,000円プラン
  • 「肘」コースの料金
    • 500円プラン
    • 50,000円プラン

ワイ「500円の次、いきなり50,000円かい」
ワイ「もっと中間のプランはないんかい」
ワイ「そんで、その料金一覧ページがどないしたん」

娘「あのね?」

タブ切り替え機能を作っていた

スクリーンショット 2023-06-28 12.48.04.png

娘「↑こんな感じの、タブ切り替えの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円でマッサージします!`
}

娘「そして、以下のようなメッセージを画面に表示するの」

スクリーンショット 2023-06-28 13.25.58.png
スクリーンショット 2023-06-28 13.38.46.png

ワイ「なんやそれ」
ワイ「右肩だけマッサージしてもらって何が嬉しいねん」
ワイ「左肩だけめっちゃ凝ってる状態、逆にイヤやわ」

娘「とにかく、引数として"肩""肘"という文字列だけを受け取る関数なの」
娘「なんだけど、実際にこの関数を実行しようとすると、TypeScriptのエラーが出ちゃうの」

スクリーンショット 2023-06-28 11.24.36.png

エラー内容
型 'string' の引数を型 '"肩" | "肘"' のパラメーターに割り当てることはできません。

ワイ「へぇ〜」

娘「courseNameMap.kataの中身は"肩"っていう文字列だから」
娘「この関数に渡せるはずなのに!」
娘「"肩"じゃなくてstring型、っていう扱いになっちゃってるからダメみたい」

ワイ「なるほどな?」
ワイ「ほな、asを使ってあげればエラーは消えるで」

<p>{ createMessage(courseNameMap.kata as "" | "") }</p>

ワイ「↑こうや」

娘「でも、それだと、TypeScriptのコンパイラを騙すことになっちゃうから」
娘「↓こういうこともできちゃうでしょ?」

{/* "首"を渡しても型エラーにならない */}
<p>{ createMessage(courseNameMap.kubi as "" | "") }</p>

娘「そうすると、画面にこんなことが表示されちゃうよ・・・!」

スクリーンショット 2023-06-28 11.58.25.png

ワイ「Oh・・・」
ワイ「首が複数本ある感じになってしまうな」

娘「そうなの」
娘「そんな人間、3組の多首(おおくび)君くらいでしょ?」

ワイ「いや3組に存在すんのかい」
ワイ「とにかく、asはアカンな・・・」
ワイ「ほな、as constを使えばええんちゃうか?」

-   const courseNameMap: Record<Course, string> = {
+   const courseNameMap = {
      kubi: "",
      kata: "",
      hiji: "",
-   }
+   } as const

ワイ「↑こうや」

ワイ「こうしてやれば・・・」

スクリーンショット 2023-06-28 11.29.24.png

ワイ「ほら、型エラーが消えたで」

娘「へぇ〜、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

娘「↑こうだね!」

ワイ「娘ちゃん、アカンで!」
ワイ「hizapizzaになってもうてるわ」
ワイ「ピッツァをマッサージしてどないすんねん」
ワイ「手がベチャベチャなってまうわ」

娘「うぅ・・・間違えた・・・」
娘「せっかく、キー名をタイポしないように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>

娘「↑こうしてやれば・・・」

スクリーンショット 2023-06-28 12.14.26.png

エラー内容
〜省略〜
'pizza' は型 Record<Course, string> に存在しません。

娘「ほら、Course型の中に存在しないキー名を書いてしまった場合に」
娘「ちゃんと型エラーで気づくことができるの!」

スクリーンショット 2023-06-28 12.50.56.png

娘「↑ちゃんと具体的な型推論もされてるよ!」

ワイ「おお〜」
ワイ「Course型の制限も効かせつつ」
ワイ「as constの恩恵も受けられるんやな」

娘「そうだね!」

ワイ「これは地味に便利やなぁ・・・!」
ワイ「ワイも勉強になったわ」
ワイ「ありがとうやで、娘ちゃん!」

その日の夜

ワイ「いや〜、TypeScriptってまだまだ進化してるんやなぁ」

娘「そうだね、地味に便利な機能がだんだん増えてくよね」

ワイ「それに比べて、ワイの能力は最近ほとんど進化してへんわ・・・」
ワイ「こんなんで、この先エンジニアとしてやって行けるんやろか・・・」
ワイ「若手エンジニアたちも、ワイなんかより全然優秀やし・・・」
ワイ「最近はChatGPTに職を奪われる心配もあるし・・・」
ワイ「不安や・・・不安や・・・」
ワイ「いや、アカン」
ワイ「あんまり不安を感じないようにせんと・・・」
ワイ「ポジティブ!ポジティブや!」

娘「不安でいいんだよ」

ワイ「えっ・・・」

娘「不安だって、必要な感情だから脳が与えてくれてるんだよ」
娘「失敗しないために。悪い未来を避けるために」

ワイ「そうか・・・」

娘「この先も、ちゃんとエンジニアとして活躍したい」
娘「そんな気持ちの裏返しだからね」
娘「全否定することじゃないんだよ」

ワイ「確かにな」
ワイ「不安はいつも、ワイを翻弄してくる存在やと思ってたけど」
ワイ「ワイを導いてくれる存在でもあるのかもな」

娘「うん」

ワイ「そう考えたら、少し楽になったわ」
ワイ「ありがとうやで、娘ちゃん・・・」

〜おしまい〜

新しい記事もよろしくやで!

136
70
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
136
70