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

# 使用技術

- 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295452/ce7532b7-cdc5-b9ad-1897-1d8e81815ae2.png)


娘「↑こんな感じの、タブ切り替えのUIがあるの」

ワイ「なるほどな」

> ユーザー「首のコースだけを絞り込んで表示したい！」

ワイ「↑こんなときに、タブを切り替えて表示できるんやな」

娘「そう」
娘「で、このタブUIを表示するために、あるオブジェクトを定義したの」

```ts
const courseNameMap: Record<Course, string> = {
  kubi: "首",
  kata: "肩",
  hiji: "肘",
}
```

ワイ「ほうほう」
ワイ「この`Course`って型は何なん？」

娘「`Course`型の中身は↓これだよ？」

```ts
type Course = "kubi" | "kata" | "hiji"
```

娘「これは`openapi.yaml`から自動生成した型なの」

ワイ「おお、ちゃんと`openapi.yaml`も書いてるんか」

娘「ううん、`openapi.yaml`は」
娘「お友達のサーバサイド・エンジニアの**手荒**（てぃあら）ちゃんが書いてくれたの」

ワイ「**手荒**（てぃあら）ちゃん・・・？」
ワイ「えらい名前やな」
ワイ「とにかく、`openapi.yaml`から自動生成したTypeScriptの型を使ってるんやな」

娘「そうそう」
娘「その型をオブジェクトのキーとして使ってあげれば、タイポの心配もないからね」

ワイ「なるほどな」
ワイ「`Record<Course, string>`って部分やな」

娘「うん」

## コースの分だけ、タブUIを表示する

娘「そして、さっきの`courseNameMap`っていうオブジェクトを使って」
娘「こんな感じでタブUIを実装してるの」

```tsx
<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円キャンペーン**っていうのを開催してて」

ワイ「ほうほう」

娘「そのキャンペーン用のメッセージを生成するための、ある関数を作ってたの」
娘「引数として`"肩"`か`"肘"`という文字列だけを受けとる関数なの」

```ts
const createMessage = (str: "肩" | "肘") => {
  return `右の${str}だけなら0円でマッサージします！`
}
```

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

![スクリーンショット 2023-06-28 13.25.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295452/7c28f169-2d0e-d9d7-e2b6-905cc6124569.png)
![スクリーンショット 2023-06-28 13.38.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295452/11162f47-804e-6d2e-28b7-eb2adf79d3c5.png)



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

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

![スクリーンショット 2023-06-28 11.24.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295452/3d5b5835-1f78-b925-284d-8892ac225f0f.png)

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

ワイ「へぇ〜」

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

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

```tsx
<p>{ createMessage(courseNameMap.kata as "肩" | "肘") }</p>
```

ワイ「↑こうや」

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

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

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

![スクリーンショット 2023-06-28 11.58.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295452/21d67678-cf86-80c9-f329-955c003f8af7.png)

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

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

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

```diff_typescript
-   const courseNameMap: Record<Course, string> = {
+   const courseNameMap = {
      kubi: "首",
      kata: "肩",
      hiji: "肘",
-   }
+   } as const
```

ワイ「↑こうや」

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


![スクリーンショット 2023-06-28 11.29.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295452/5cda8a0f-7720-9b80-cc9c-9b1f6e3ee166.png)

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

娘「へぇ〜、`as const`をつけると」
娘「`courseNameMap.kata`が、`string`じゃなくて`"肩"`だって」
娘「具体的な型を推論してもらえるんだね！」

ワイ「せやで」
ワイ「具体的なリテラル型を推論してくれるんや」

娘「でもパパ」
娘「どうして`Record<Course, string>`っていう型注釈を消しちゃったの？」

ワイ「`as const`の恩恵を受けたい場合は、`Record<Course, string>`っていう型注釈は併用できひんのや」

娘「そうなんだ・・・」
娘「まぁ、いっか！」

# しかし、次の週・・・

ワイ「お、娘ちゃん」
ワイ「また**ポキポキ**さんのWebサイトをいじってるんか？」

娘「うん」
娘「実はね、お店の名前が微妙に変わったの！」

> #### 【旧】
>「首・肩・肘」専門マッサージ店　**ポキポキ**
>
> 　↓↓↓
>
> #### 【新】
>「首・肩・肘・腰・膝・足首」専門マッサージ店　**ポキポキ**

娘「首・肩・肘だけじゃなくて」
娘「腰・膝・足首も専門的にマッサージしてくれるようになったの！」

ワイ「いや、もうそれ**全身ですやん**」
ワイ「どこが専門やねん」
ワイ「推しポイントが弱くなってもうてるで」

娘「とにかく、Webサイトの方にも」
娘「腰・膝・足首のコースの料金を表示しないといけないの」
娘「だから今、タブUIにも**腰・膝・足首**を追加するところなの」
娘「えっと・・・」

```diff_typescript
    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`っていうのがあった！」
娘「あれを使えばいいんだ」

```diff_typescript
    const courseNameMap = {
      kubi: "首",
      kata: "肩",
      hiji: "肘",
      koshi: "腰",
      pizza: "膝",
      ashikubi: "足首",
-   } as const
+   } as const satisfies Record<Course, string>
```

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

![スクリーンショット 2023-06-28 12.14.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295452/4a5806f3-5c72-56a7-b63d-59d5e3ebf03f.png)

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

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

![スクリーンショット 2023-06-28 12.50.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295452/1c441ac4-167a-723f-291f-699df87f7b2e.png)

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

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

娘「そうだね！」

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

# その日の夜

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

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

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

娘「不安でいいんだよ」

ワイ「えっ・・・」

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

ワイ「そうか・・・」

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

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

娘「うん」

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

**〜おしまい〜**

# 新しい記事もよろしくやで！

- **[親コンポーネントがステートを持つべきなの？それとも子コンポーネント？](https://qiita.com/Yametaro/items/e812377c380a7fe5a203)**