実戦 型パズル
Tadashi Aikawa
2024/11/17 Minerva Lightning Talks

Tadashi Aikawa

Productivity Creator since 2010
OS
Windows (開発はUbuntu on WSL)
言語
TypeScript >> Python = Go = Rust > Lua
エディタ
Neovim / Obsidian
デバイス
EIZO / HHKB Studio / SlimBlade
好き
創作活動・温泉・甘味・動物(ぬいぐるみ含む)
苦手
お酒・車・勉強
楽しい仕事
個人やチームの生産性を上げて成果に繋げる

Web系 年表

2010年
エンジニアとして仕事をはじめる
2012年
はじめてJavaScriptでモノをつくる
2015年
はじめてReactでモノをつくる
2016年
はじめてTypeScriptでモノをつくる
2017年
はじめてAngular2でモノをつくる
2018年
はじめてVue2でモノをつくる
はじめてVSCode拡張をつくる
2019年
はじめてChrome拡張をつくる
2020年
はじめてVue3でモノをつくる
はじめてSvelteでモノをつくる
2021年
はじめてObsidianプラグインをつくる
2023年
はじめてDenoでモノをつくる
はじめてBunでモノをつくる
2024年
はじめてNeovimプラグインをつくる

よくあるVueの選択要素実装(select)を通して

実戦で使えるTypeScriptの型スキルを学ぼう

本日のお題

25分のネタ9分で話します

分かっている人がなんとかついていけるレベルかなと

ケーキを選択するvueファイル

<script setup lang="ts">
import { computed, ref } from "vue";

const cakeList = [
  { name: "ショートケーキ", fruit: "イチゴ" },
  { name: "チーズケーキ" },
];

const myCake = ref<any>("");

const message = computed(() => {
  switch (myCake.value.name) {
    case "ショートケーキ":
      return `上は ${myCake.value.fruit} だ!`;
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
  }
});
</script>
<template>
  <select v-model="myCake">
    <option v-for="cake in cakeList" :value="cake">
      {{ cake.name }}
    </option>
  </select>
  <h1>{{ message }}</h1>
</template>

TypeScriptコードの方に着目

<script setup lang="ts">
import { computed, ref } from "vue";

const cakeList = [
  { name: "ショートケーキ", fruit: "イチゴ" },
  { name: "チーズケーキ" },
];

const myCake = ref<any>("");

const message = computed(() => {
  switch (myCake.value.name) {
    case "ショートケーキ":
      return `上は ${myCake.value.fruit} だ!`;
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
  }
});
</script>

さらにVueに関する部分を除外

const cakeList = [
  { name: "ショートケーキ", fruit: "イチゴ" },
  { name: "チーズケーキ" },
];

let myCake: any = "";

myCake = cakeList[0]; // 1番目の要素(=ショートケーキ)を選んだと仮定した処理

function getMessage() {
  switch (myCake.name) {
    case "ショートケーキ":
      return `上は ${myCake.fruit} だ!`;
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
  }
}

何が問題か?

変数myCakeをany型と宣言していることが問題

const cakeList = [
  { name: "ショートケーキ", fruit: "イチゴ" },
  { name: "チーズケーキ" },
];

let myCake: any = ""; // myCakeをany型と宣言 (そもそもなぜ空文字?)
myCake = cakeList[0]; // 代入後もany ({ name: string, fruit: string }型って知ってるのに...)

function getMessage() {
  switch (myCake.name) { // myCake.nameもany
    case "ショートケーキ":
      return `上は ${myCake.fruit} だ!`; // myCake.fruitもany
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
  }
}

実際に起こる悲劇

const cakeList = [ // 中身を変更してみる (『string[]でOk!』と勘違い...)
  "ショートケーキ",
  "チーズケーキ",
];

let myCake: any = "";
myCake = cakeList[0];

function getMessage() {
  switch (myCake.name) {
    case "ショートケーキ":
      return `上は ${myCake.fruit} だ!`;
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
  }
}

意図通り動かない。エラーも出ない。

const cakeList = [
  "ショートケーキ",
  "チーズケーキ",
];

let myCake: any = "";
myCake = cakeList[0];

function getMessage() {
  switch (myCake.name) { // undefinedになる
    case "ショートケーキ":
      return `上は ${myCake.fruit} だ!`; // ここには永遠に来ない...
    case "チーズケーキ":
      return `チーズケーキおいしい!`; // ここには永遠に来ない...
  }
}

どうすればいいの?

Cake型を定義し、変数myCakeの型はCake型だと宣言する

const cakeList = [ "ショートケーキ", "チーズケーキ" ];

type Cake = { // Cake型の宣言
  name: string;   // 名前は必須
  fruit?: string; // フルーツが乗ってるかも?
}

let myCake: Cake = { name: "チーズケーキ" }; // any -> Cake に変更して、初期値も設定
myCake = cakeList[0];

function getMessage() {
  switch (myCake.name) {
    case "ショートケーキ":
      return `上は ${myCake.fruit} だ!`;
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
  }
}

エラーが出るので実行前に気づける

const cakeList = [ "ショートケーキ", "チーズケーキ" ];

type Cake = {
  name: string;
  fruit?: string;
}

let myCake: Cake = { name: "チーズケーキ" };
myCake = cakeList[0]; // ⛔ string型の値(ショートケーキ) は Cake型の変数(myCake) に入れられないのでエラー!

function getMessage() {
  switch (myCake.name) {
    case "ショートケーキ":
      return `上は ${myCake.fruit} だ!`;
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
  }
}

ウォーミングアップ終了

cakeListを元に戻して再開

const cakeList = [
  { name: "ショートケーキ", fruit: "イチゴ" },
  { name: "チーズケーキ" },
];

type Cake = {
  name: string;
  fruit?: string;
};

let myCake: Cake = { name: "チーズケーキ" };
myCake = cakeList[0];

function getMessage() {
  switch (myCake.name) {
    case "ショートケーキ":
      return `上は ${myCake.fruit} だ!`;
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
  }
}

typoしちゃった...

const cakeList = [
  { name: "ショートケーキ", fruit: "イチゴ" },
  { name: "チーズケーキ" },
];

type Cake = {
  name: string;
  fruit?: string;
};

let myCake: Cake = { name: "チーズケーキ" };
myCake = cakeList[0];

function getMessage() {
  switch (myCake.name) {
    case "ショットケーキ": // typoしちゃった...
      return `上は ${myCake.fruit} だ!`;
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
  }
}

typoしちゃった...けどエラーは出ない

const cakeList = [
  { name: "ショートケーキ", fruit: "イチゴ" },
  { name: "チーズケーキ" },
];

type Cake = {
  name: string;
  fruit?: string;
};

let myCake: Cake = { name: "チーズケーキ" };
myCake = cakeList[0];

function getMessage() {
  switch (myCake.name) {
    case "ショットケーキ":
      return `上は ${myCake.fruit} だ!`; // ⛔ ここには永遠に入らない
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
  }
}

typoしちゃった...けどエラーは出ない

const cakeList = [
  { name: "ショートケーキ", fruit: "イチゴ" },
  { name: "チーズケーキ" },
];

type Cake = {
  name: string;
  fruit?: string;
};

let myCake: Cake = { name: "チーズケーキ" };
myCake = cakeList[0];

function getMessage() {
  switch (myCake.name) {
    case "ショットケーキ": // ここでエラーになってほしい (『ショットケーキなんてないぞ!』と)
      return `上は ${myCake.fruit} だ!`;
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
  }
}

なぜエラーにしてくれないの?

const cakeList = [
  { name: "ショートケーキ", fruit: "イチゴ" },
  { name: "チーズケーキ" },
];

type Cake = {
  name: string;  // 1.Cake型のnameプロパティはstring型である
  fruit?: string;
};

let myCake: Cake = { name: "チーズケーキ" }; // 2.myCakeはCake型 (let宣言なので右辺の値は関係ない)
myCake = cakeList[0];

function getMessage() {
  switch (myCake.name) { // 3.Cake["name"]はstring型
    case "ショットケーキ": // 4.string型なら"ショットケーキ"の可能性もあるよね
      return `上は ${myCake.fruit} だ!`;
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
  }
}

どうすればいいの?

Cake["name"]を文字列リテラルのユニオン型にする

const cakeList = [
  { name: "ショートケーキ", fruit: "イチゴ" },
  { name: "チーズケーキ" },
];

type Cake = {
  name: "ショートケーキ" | "チーズケーキ"; // 1.文字列リテラルのユニオン型 に変更
  fruit?: string;
};

let myCake: Cake = { name: "チーズケーキ" };
myCake = cakeList[0];

function getMessage() {
  switch (myCake.name) { // 2.myCake.nameの値は "ショートケーキ" or "チーズケーキ"
    case "ショットケーキ": // ⛔ 3."ショットケーキ" はあり得ないのでエラー
      return `上は ${myCake.fruit} だ!`;
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
  }
}

それだけではmyCakeの代入処理がエラーになる

const cakeList = [
  { name: "ショートケーキ", fruit: "イチゴ" }, // cakeList[0]["name"] の値はこれ -> cakeList[0].nameは可変なのでstring型と推論される)
  { name: "チーズケーキ" },
];

type Cake = {
  name: "ショートケーキ" | "チーズケーキ"; // myCake["name"] の型はこれ
  fruit?: string;
};

let myCake: Cake = { name: "チーズケーキ" };
myCake = cakeList[0]; // ⛔ cakeList[0]はmyCakeに代入できない (cakeList[0]["name"]の値はmyCake["name"]に代入できないから)

function getMessage() {
  switch (myCake.name) {
    case "ショットケーキ":
      return `上は ${myCake.fruit} だ!`;
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
  }
}

cakeListも宣言時に型を指定してあげる

const cakeList: Cake[] = [ // 1.Cake[]型 と宣言
  { name: "ショートケーキ", fruit: "イチゴ" }, // 2.cakeList[0] は Cake型 になる
  { name: "チーズケーキ" },
];

type Cake = {
  name: "ショートケーキ" | "チーズケーキ";
  fruit?: string;
};

let myCake: Cake = { name: "チーズケーキ" };
myCake = cakeList[0]; // 3.Cake型 に Cake型 の代入なので問題なし

function getMessage() {
  switch (myCake.name) {
    case "ショットケーキ": // ⛔ 4.ここもちゃんとエラーになる
      return `上は ${myCake.fruit} だ!`;
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
  }
}

swtich文を深堀する

先ほどのコードからswitch文にフォーカスして抽出する

const cakeList: Cake[] = [ 
  { name: "ショートケーキ", fruit: "イチゴ" }, 
  { name: "チーズケーキ" },
];

type Cake = {
  name: "ショートケーキ" | "チーズケーキ";
  fruit?: string;
};

let myCake: Cake = { name: "チーズケーキ" };
myCake = cakeList[0]; 

function getMessage() {
  switch (myCake.name) {
    case "ショットケーキ": 
      return `上は ${myCake.fruit} だ!`;
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
  }
}

先ほどのコードからswitch文にフォーカスして抽出する

type Cake = {
  name: "ショートケーキ" | "チーズケーキ";
  fruit?: string;
};

declare const myCake: Cake;

function getMessage() {
  switch (myCake.name) {
    case "ショートケーキ":
      return `上は ${myCake.fruit} だ!`;
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
  }
}

Cake["name"] が増えるとどうなる?

type Cake = {
  name: "ショートケーキ" | "チーズケーキ" | "モンブラン"; // "モンブラン" を追加
  fruit?: string;
};

declare const myCake: Cake;

function getMessage() {
  switch (myCake.name) {
    case "ショートケーキ":
      return `上は ${myCake.fruit} だ!`;
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
  }
}

モンブランの考慮が抜けていてもエラーにならない

type Cake = {
  name: "ショートケーキ" | "チーズケーキ" | "モンブラン";
  fruit?: string;
};

declare const myCake: Cake;

function getMessage() {
  switch (myCake.name) { // case "モンブラン" がないと明らかに実装漏れ
    case "ショートケーキ":
      return `上は ${myCake.fruit} だ!`;
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
  } // ここで『"モンブラン"のcase文がないよ!』みたいなエラーが欲しい
}

どうすればいいの?

never型を使ってExhaustiveErrorエラーをつくる

export class ExhaustiveError extends Error { // 新しく追加したエラークラス
  constructor(value: never, message = `Unsupported type: ${value}`) { // コンストラクタはnever型
    super(message);
  }
}

type Cake = {
  name: "ショートケーキ" | "チーズケーキ" | "モンブラン";
  fruit?: string;
};

declare const myCake: Cake;

function getMessage() {
  switch (myCake.name) { // 1.myCake.nameの型は "ショートケーキ" | "チーズケーキ" | "モンブラン"
    case "ショートケーキ":
      return `上は ${myCake.fruit} だ!`;
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
    default: // 2.なのでdefaultに入るのはmyCake.nameが"モンブラン"の場合のみ( = "モンブラン"型と推論)
      return new ExhaustiveError(myCake.name) // ⛔ myCake.name がnever型でない場合はエラーになる ("モンブラン型" なのでエラー)
  }
}

swtichのcase文が網羅されていればnever型となる

export class ExhaustiveError extends Error
  constructor(value: never, message = `Unsupported type: ${value}`)
    super(message);
  }
}

type Cake = {
  name: "ショートケーキ" | "チーズケーキ" | "モンブラン";
  fruit?: string;
};

declare const myCake: Cake;

function getMessage() {
  switch (myCake.name) { // 1.myCake.nameの型は "ショートケーキ" | "チーズケーキ" | "モンブラン"
    case "ショートケーキ":
      return `上は ${myCake.fruit} だ!`;
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
    case "モンブラン": // 追加
      return `モンブランはやっぱり ${myCake.fruit} よね!`;
    default: // 2.なのでdefaultの中には絶対に入らないはず...
      return new ExhaustiveError(myCake.name) // 3.よってmyCake.nameはnever型と推論されるためエラーは消える
  }
}

次の話でフォーカスしないコードを隠す

export class ExhaustiveError extends Error
  constructor(value: never, message = `Unsupported type: ${value}`)
    super(message);
  }
}

type Cake = {
  name: "ショートケーキ" | "チーズケーキ" | "モンブラン";
  fruit?: string;
};

declare const myCake: Cake;

function getMessage() {
  switch (myCake.name) { // 1.myCake.nameの型は "ショートケーキ" | "チーズケーキ" | "モンブラン"
    case "ショートケーキ":
      return `上は ${myCake.fruit} だ!`;
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
    case "モンブラン": // 追加
      return `モンブランはやっぱり ${myCake.fruit} よね!`;
    default: // 2.なのでdefaultの中には絶対に入らないはず...
      return new ExhaustiveError(myCake.name) // 3.よってmyCake.nameはnever型と推論されるためエラーは消える
  }
}

次の話でフォーカスしないコードを隠す

type Cake = {
  name: "ショートケーキ" | "チーズケーキ";
  fruit?: string;
};

declare const myCake: Cake;

function getMessage() {
  switch (myCake.name) {
    case "ショートケーキ":
      return `上は ${myCake.fruit} だ!`;
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
    default:
      return new ExhaustiveError(myCake.name);
  }
}

このコードにはまだ問題がある

type Cake = {
  name: "ショートケーキ" | "チーズケーキ";
  fruit?: string;
};

declare const myCake: Cake;

function getMessage() {
  switch (myCake.name) {
    case "ショートケーキ":
      return `上は ${myCake.fruit} だ!`;
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
    default:
      return new ExhaustiveError(myCake.name);
  }
}

ショートケーキのfruitがイチゴであると推論してくれない

type Cake = {
  name: "ショートケーキ" | "チーズケーキ";
  fruit?: string;
};

declare const myCake: Cake;

function getMessage() {
  switch (myCake.name) {
    case "ショートケーキ":
      return `上は ${myCake.fruit} だ!`; // myCake.fruit は string | undefined型 と推論される
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
    default:
      return new ExhaustiveError(myCake.name);
  }
}

現状の型定義では2通りまでNarrowingされない

type Cake = {
  name: "ショートケーキ" | "チーズケーキ";
  fruit?: string;
};

const cakes: Cake[] = [ // 今の定義では以下6パターンのいずれもOKとなってしまう
  { name: "ショートケーキ" }, // エラーにしたい!
  { name: "ショートケーキ", fruit: "イチゴ" },
  { name: "ショートケーキ", fruit: "マロン" }, // エラーにしたい!
  { name: "チーズケーキ" },
  { name: "チーズケーキ", fruit: "イチゴ" }, // エラーにしたい!
  { name: "チーズケーキ", fruit: "マロン" }, // エラーにしたい!
];

どうすればいいの?

判別されたユニオン型を使う

type SpongeCake = {
  name: "ショートケーキ"; // 共通のプロパティ
  fruit: "イチゴ";
};
type Cheesecake = {
  name: "チーズケーキ"; // 共通のプロパティ
};
type Cake = SpongeCake | Cheesecake; // Cakeは判別用の共通プロパティ(name)をもつユニオン型である

const cakes: Cake[] = [
  { name: "ショートケーキ" },
  { name: "ショートケーキ", fruit: "イチゴ" },
  { name: "ショートケーキ", fruit: "マロン" },
  { name: "チーズケーキ" },
  { name: "チーズケーキ", fruit: "イチゴ" },
  { name: "チーズケーキ", fruit: "マロン" },
];

SpongeCake型でもCheesecake型でもない値はエラーになる

type SpongeCake = {
  name: "ショートケーキ";
  fruit: "イチゴ";
};
type Cheesecake = {
  name: "チーズケーキ";
};
type Cake = SpongeCake | Cheesecake;

const cakes: Cake[] = [
  { name: "ショートケーキ" },                  // ⛔ エラーになる (SpongeCake型なのにfruitがない)
  { name: "ショートケーキ", fruit: "イチゴ" },
  { name: "ショートケーキ", fruit: "マロン" }, // ⛔ エラーになる (.fruitの値が"イチゴ"ではない)
  { name: "チーズケーキ" },
  { name: "チーズケーキ", fruit: "イチゴ" },   // ⛔ エラーになる (Cheesecake型なのにfruitがある)
  { name: "チーズケーキ", fruit: "マロン" },   // ⛔ エラーになる (Cheesecake型なのにfruitがある)
];

ショートケーキのfruitがイチゴであると推論してくれない

type Cake = {
  name: "ショートケーキ" | "チーズケーキ";
  fruit?: string;
};

declare const myCake: Cake;

function getMessage() {
  switch (myCake.name) {
    case "ショートケーキ":
      return `上は ${myCake.fruit} だ!`; // myCake.fruit は string | undefined型 と推論される
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
    default:
      return new ExhaustiveError(myCake.name);
  }
}

再掲

判別されたユニオン型に変更

type SpongeCake = {
  name: "ショートケーキ";
  fruit: "イチゴ";
};
type Cheesecake = {
  name: "チーズケーキ";
};
type Cake = SpongeCake | Cheesecake;

declare const myCake: Cake;

function getMessage() {
  switch (myCake.name) { // 1.myCake.name は "ショートケーキ" | "チーズケーキ"型と推論される
    case "ショートケーキ":
      return `上は ${myCake.fruit} だ!`; // 2.myCake.fruit は "イチゴ"型 と推論される! (myCakeがSpongeCake型と推論されたから)
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
    default: // 3.ここにくるのはあり得ない -> myCake は never型 と推論される
      return new ExhaustiveError(myCake); // myCake.name -> myCake に変更 (詳細は時間の都合で割愛)
  }
}

switch文からフォーカスを解く

type SpongeCake = {
  name: "ショートケーキ";
  fruit: "イチゴ";
};
type Cheesecake = {
  name: "チーズケーキ";
};
type Cake = SpongeCake | Cheesecake;

declare const myCake: Cake;

function getMessage() {
  switch (myCake.name) {
    case "ショートケーキ":
      return `上は ${myCake.fruit} だ!`;
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
    default:
      return new ExhaustiveError(myCake);
  }
}

switch文からフォーカスを解く

const cakeList: Cake[] = [
  { name: "ショートケーキ", fruit: "イチゴ" },
  { name: "チーズケーキ" },
];

type SpongeCake = {
  name: "ショートケーキ";
  fruit: "イチゴ";
};
type Cheesecake = {
  name: "チーズケーキ";
};
type Cake = SpongeCake | Cheesecake;

let myCake: Cake = { name: "チーズケーキ" };
myCake = cakeList[0];

function getMessage() {
  switch (myCake.name) {
    case "ショートケーキ":
      return `上は ${myCake.fruit} だ!`;
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
    default:
      return new ExhaustiveError(myCake); // (ExhaustiveErrorの定義は割愛)
  }
}

今度は型定義にフォーカスしてみる

型定義部分にだけフォーカス

const cakeList: Cake[] = [
  { name: "ショートケーキ", fruit: "イチゴ" },
  { name: "チーズケーキ" },
];

type SpongeCake = {
  name: "ショートケーキ";
  fruit: "イチゴ";
};
type Cheesecake = {
  name: "チーズケーキ";
};
type Cake = SpongeCake | Cheesecake;

let myCake: Cake = { name: "チーズケーキ" };
myCake = cakeList[0];

function getMessage() {
  switch (myCake.name) {
    case "ショートケーキ":
      return `上は ${myCake.fruit} だ!`;
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
    default:
      return new ExhaustiveError(myCake); 
  }
}

型定義部分にだけフォーカス

const cakeList: Cake[] = [
  { name: "ショートケーキ", fruit: "イチゴ" },
  { name: "チーズケーキ" },
];

type SpongeCake = {
  name: "ショートケーキ";
  fruit: "イチゴ";
};
type Cheesecake = {
  name: "チーズケーキ";
};
type Cake = SpongeCake | Cheesecake;

ケーキの種類を増やしてみる

const cakeList: Cake[] = [
  { name: "ショートケーキ", fruit: "イチゴ" },
  { name: "チーズケーキ" },
  { name: "モンブラン", fruit: "栗" }, // 追加
  { name: "チョコレートケーキ" }, // 追加
];

type SpongeCake = {
  name: "ショートケーキ";
  fruit: "イチゴ";
};
type Cheesecake = {
  name: "チーズケーキ";
};
type MontBlanc = { // 追加
  name: "モンブラン";
  fruit: "栗";
};
type ChocolateCake = { // 追加
  name: "チョコレートケーキ";
};
type Cake = SpongeCake | Cheesecake | MontBlanc | ChocolateCake; // 追加

なんか面倒くさくない...?

候補が決まっているなら変更は最低限にしたい

const cakeList = [ // cakeListに追加したらCake型に自動で反映されてほしい
  { name: "ショートケーキ", fruit: "イチゴ" },
  { name: "チーズケーキ" },
];

type Cake = ??? // ここをイイ感じに定義して1行で済ませたい

どうすればいいの?

typeof型演算子とインデックスアクセス型を使う

const cakeList = [
  { name: "ショートケーキ", fruit: "イチゴ" },
  { name: "チーズケーキ" },
];

type Cake = (typeof cakeList)[number]; // typeof型演算子でcakeList(値)から型を生成し、[number]でその配列要素の型を表現する

typeof型演算子とインデックスアクセス型を使う

const cakeList = [
  { name: "ショートケーキ", fruit: "イチゴ" },
  { name: "チーズケーキ" },
];

type Cake = (typeof cakeList)[number]; // typeof型演算子でcakeList(値)から型を生成し、[number]でその配列要素の型を表現する

推論の過程

type (typeof cakeList) = ({ // 配列と推論
    name: string;
    fruit: string;
} | {
    name: string;
    fruit?: undefined;
})[]

typeof型演算子インデックスアクセス型を使う

const cakeList = [
  { name: "ショートケーキ", fruit: "イチゴ" },
  { name: "チーズケーキ" },
];

type Cake = (typeof cakeList)[number]; // typeof演算子でcakeList(値)から型を生成し、[number]でその配列要素の型を表現する

推論の過程

type (typeof cakeList) = ({
    name: string;
    fruit: string;
} | {
    name: string;
    fruit?: undefined;
})[]
type (typeof cakeList)[number] = {
    name: string;
    fruit: string;
} | {
    name: string;
    fruit?: undefined;
}

constは代入した値を不変にするわけではない

const cakeList = [ // cakeListの値は変更できるため (typeof cakeList)[number]["name"] は string型 よりもNarrowingされない
  { name: "ショートケーキ", fruit: "イチゴ" },
  { name: "チーズケーキ" },
];

type Cake = (typeof cakeList)[number];

推論の過程

type (typeof cakeList) = ({
    name: string;
    fruit: string;
} | {
    name: string;
    fruit?: undefined;
})[]
type (typeof cakeList)[number] = {
    name: string;
    fruit: string;
} | {
    name: string;
    fruit?: undefined;
}

as const で cakeListを不変にする

const cakeList = [
  { name: "ショートケーキ", fruit: "イチゴ" },
  { name: "チーズケーキ" },
] as const; // as const を追加

type Cake = (typeof cakeList)[number];

as const で cakeListを不変にする

const cakeList = [
  { name: "ショートケーキ", fruit: "イチゴ" },
  { name: "チーズケーキ" },
] as const; // as const を追加

type Cake = (typeof cakeList)[number];

推論の過程

type (typeof cakeList) = readonly [{ // 配列ではなくタプル型に推論される!
    readonly name: "ショートケーキ";
    readonly fruit: "イチゴ";
}, {
    readonly name: "チーズケーキ";
}]

as const で cakeListを不変にする

const cakeList = [
  { name: "ショートケーキ", fruit: "イチゴ" },
  { name: "チーズケーキ" },
] as const; // as const を追加

type Cake = (typeof cakeList)[number];

推論の過程

type (typeof cakeList) = readonly [{
    readonly name: "ショートケーキ";
    readonly fruit: "イチゴ";
}, {
    readonly name: "チーズケーキ";
}]
type (typeof cakeList)[number] = {
    readonly name: "ショートケーキ";
    readonly fruit: "イチゴ";
} | {
    readonly name: "チーズケーキ";
}

話を戻すと...

ケーキの種類を増やすために必要な型と作業が...

const cakeList: Cake[] = [
  { name: "ショートケーキ", fruit: "イチゴ" },
  { name: "チーズケーキ" },
  { name: "モンブラン", fruit: "栗" }, // 追加
  { name: "チョコレートケーキ" }, // 追加
];

type SpongeCake = {
  name: "ショートケーキ";
  fruit: "イチゴ";
};
type Cheesecake = {
  name: "チーズケーキ";
};
type MontBlanc = { // 追加
  name: "モンブラン";
  fruit: "栗";
};
type ChocolateCake = { // 追加
  name: "チョコレートケーキ";
};
type Cake = SpongeCake | Cheesecake | MontBlanc | ChocolateCake; // 追加

実際はここまでスリム化できる

const cakeList: Cake[] = [
  { name: "ショートケーキ", fruit: "イチゴ" },
  { name: "チーズケーキ" },
  { name: "モンブラン", fruit: "栗" },
  { name: "チョコレートケーキ" },
] as const; // as const 追加

type Cake = (typeof cakeList)[number]; // typeof型演算子とインデックスアクセス型で表現

ただ、一部の条件に当てはまる場合に限る

const cakeList: Cake[] = [
  { name: "ショートケーキ", fruit: "イチゴ" },
  { name: "チーズケーキ" },
  { name: "モンブラン", fruit: "栗" },
  { name: "チョコレートケーキ" },
] as const; // as const 追加

type Cake = (typeof cakeList)[number]; // typeof型演算子とインデックスアクセス型で表現
  • 条件
    • ビルド時に選択肢(cakeList)が確定している
      • APIからデータ取得する場合などは使えない
    • Cheesecake型など個々の型を利用することがない
      • 個々の型を利用するなら判別されたユニオン型を使った方がいい

Vueファイルに適当してみよう

TypeScriptのコードのみ (before)

<script setup lang="ts">
import { computed, ref } from "vue";

const cakeList = [
  { name: "ショートケーキ", fruit: "イチゴ" },
  { name: "チーズケーキ" },
];

const myCake = ref<any>("");

const message = computed(() => {
  switch (myCake.value.name) {
    case "ショートケーキ":
      return `上は ${myCake.value.fruit} だ!`;
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
  }
});
</script>

TypeScriptのコードのみ (after)

<script setup lang="ts">
import { computed, ref } from "vue";
import { ExhaustiveError } from "./errors"; // ExhaustiveErrorは外部からimport

const cakeList = [
  { name: "ショートケーキ", fruit: "イチゴ" },
  { name: "チーズケーキ" },
] as const; // as const 追加
type Cake = (typeof cakeList)[number]; // Cake型の定義追加

const myCake = ref<Cake>(cakeList[0]); // 初期値を挿入

const message = computed(() => {
  switch (myCake.value.name) {
    case "ショートケーキ":
      return `上は ${myCake.value.fruit} だ!`;
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
    default: // defaultとExhaustiveErrorによる網羅性チェックを追加
      return new ExhaustiveError(myCake.value);
  }
});
</script>

全体

<script setup lang="ts">
import { computed, ref } from "vue";
import { ExhaustiveError } from "./errors";

const cakeList = [
  { name: "ショートケーキ", fruit: "イチゴ" },
  { name: "チーズケーキ" },
] as const;
type Cake = (typeof cakeList)[number];

const myCake = ref<Cake>(cakeList[0]);

const message = computed(() => {
  switch (myCake.value.name) {
    case "ショートケーキ":
      return `上は ${myCake.value.fruit} だ!`;
    case "チーズケーキ":
      return `チーズケーキおいしい!`;
    default:
      return new ExhaustiveError(myCake.value);
  }
});
</script>
<template>
  <select v-model="myCake">
    <option v-for="cake in cakeList" :value="cake">
      {{ cake.name }}
    </option>
  </select>
  <h1>{{ message }}</h1>
</template>
-- src/errors.ts
export class ExhaustiveError extends Error {
  constructor(
    value: never,
    message = `Unsupported type: ${value}`
  ) {
    super(message);
  }
}

まとめ

  • 文字列リテラルのユニオン型を使って候補をstringよりNarrowingしよう
  • ExhausitiveErrorを使ってcaseの実装漏れを防ごう
  • 可変候補には 判別されたユニオン型を使ってNarrowingしよう (APIなど)
  • 固定候補には typeof型演算子 + インデックスアクセス型 + as const を使おう

おたよりまってますぞー

仕事だったら『所属』『代表プロダクト』『入社年』などを入れる