【入門編】 neverを用いた型のフィルタリング – TypeScript実践ガイド

「この世には存在しない型」neverで、もう不要な型をスパッと切り捨てる方法! ~Conditional Typesの裏技~

こんにちは!Web制作・開発の世界へようこそ!TypeScriptって、最初は「型?何それ美味しいの?」って感じだったり、ちょっと難しそう…って尻込みしちゃったりしますよね。でも大丈夫!この世界は、一度コツを掴むと驚くほど快適に、そして安全に開発が進められるようになるんです。

今日は、そんなTypeScriptのちょっとした「裏技」のような、でも知っておくと「なるほど!」って膝を打つような、そんなテクニックをご紹介します。「never」っていう、ちょっと不思議な型を使った「型のフィルタリング」というお話です。

そもそも「型」って何?なんで大事なの?

まず、TypeScriptの「型」について、軽くおさらいしておきましょう。

例えるなら、型は「道具箱の中の道具に名前をつける」ようなものです。

  • string: これは「文字」を入れるための箱。名前とか、文章とか。
  • number: これは「数字」を入れるための箱。値段とか、個数とか。
  • boolean: これは「真偽」(はい/いいえ、true/false)を入れるための箱。チェックボックスがオンかオフか、とか。
  • array: これは「同じ種類の箱をたくさん並べたもの」。例えば、数字の箱がずらーっと並んでいるとか。
  • tuple: これは「決まった数の、決まった種類の箱を並べたもの」。例えば、「名前(string)」「年齢(number)」「性別(string)」のように、順番と種類が決まっている場合。

これらの「型」をちゃんとつけてあげることで、

  • 「あれ?ここは数字を入れるはずなのに、間違って文字を入れちゃった!」
  • 「この関数は、必ず数字を返してくれるはずなのに、なぜか文字列が返ってきた…」

なんていう、開発中に起こりがちな「うっかりミス」を、コードを書いている途中で(あるいは実行する前に!)教えてくれるんです。まるで、親切なアシスタントさんが「そこ、違いますよ!」って教えてくれるみたいですよね。

「any」と「unknown」:万能選手だけど、ちょっと注意が必要な二人組

型には、色々な種類があるんですが、今日は特に「never」という型に焦点を当てるために、まず「any」と「unknown」について触れておきましょう。

  • any: これは「どんな型でもOK!」っていう、まさに万能選手。でも、あまりにも自由すぎて、型チェックをしてくれるTypeScriptのメリットを半減させてしまうことも。ちょっと危ない橋を渡るような感覚かもしれません。
  • unknown: これは「any」と似ているけれど、もう少し慎重な万能選手。「何が入ってくるかわからないけど、ちゃんと確認してから使ってね!」という意思表示。これも便利ですが、今日の主役はもっと違うんです。

いよいよ本題!「never」って一体何者? ~「決して到達しない場所」の型~

さて、今日の本題「never」型です。

これは、文字通り 「決して到達しない場所」 を表す型なんです。え?どういうこと?って思いますよね。

例えるなら、

  • 「このお店は、もう閉店しました。二度と開くことはありません。」
  • 「このボタンは、壊れてしまっていて、押しても何も起こりません。」

みたいな、「もう、ここには来ない」「ここには何も入らない」という状態を表現する型なんです。

コードの世界で言うと、例えば、

  • エラーを発生させて、処理をそこで強制終了させる
  • 関数から決して値を返さない(無限ループに陥るとか)

といった場合に、「この先には、もう何も値は来ないよ」という意味で `never` 型が使われることがあります。

Conditional Typesで「型のお買い物」をしてみよう! ~不要なものを賢く選別~

さて、この「never」型が、どうやって「不要な型をフィルタリングする」のに役立つのか?
ここで登場するのが 「Conditional Types(条件付き型)」 です。

Conditional Typesは、TypeScriptの高度な型機能の一つで、ある条件を満たすかどうかで、型を切り替えることができます。

例えるなら、お買い物に行った時のレジでのやり取りに似ています。

「この商品は、会員証をお持ちですか?」
→「はい、持っています」→ 特別価格に!
→「いいえ、持っていません」→ 定価でどうぞ。

というように、条件によって結果が変わりますよね。

TypeScriptのConditional Typesも、これと似たようなことができます。

// T が U に含まれる型かどうかをチェックする
// もし含まれるなら T を返す。含まれないなら never を返す。
type Filter = T extends U ? T : never;

この `Filter` という型を見てみましょう。

  • `T`:チェックしたい型(例えば、`string`とか`number`とか)
  • `U`:比較対象となる型(例えば、`string | number`のようなユニオン型)

このConditional Typesは、

「もし `T` が `U` の中に含まれる型だったら、そのまま `T` を返してね。」
「もし `T` が `U` の中に含まれない型だったら、`never` を返してね。」

という指示を出しています。

「never」が活躍する瞬間! ~ユニオン型から不要な型を「ポイッ」~

では、この `Filter` 型を、具体的な例で使ってみましょう。
例えば、こんなユニオン型があるとします。

type Fruit = “apple” | “banana” | “orange” | “grape”;

これは、「りんご」「バナナ」「オレンジ」「ぶどう」という、4つの果物の名前(文字列)が入る可能性のある箱だと想像してください。

ここで、もし「りんご」と「バナナ」だけが欲しい、という状況になったとします。
「オレンジ」と「ぶどう」は、今回は不要!

そこで、先ほどの `Filter` 型を使って、「不要なもの」を `never` に変換してみましょう。

// Fruit 型の中から、”apple” または “banana” に合致しないものを除外したい
type OnlyAppleAndBanana = Filter;

さて、この `OnlyAppleAndBanana` という型は、一体どんな型になるでしょうか?
Conditional Typesは、ユニオン型のそれぞれの要素に対して、このチェックを繰り返します。

1. `”apple”` は `”apple” | “banana”` に含まれるか? → はい → `”apple”` を返す
2. `”banana”` は `”apple” | “banana”` に含まれるか? → はい → `”banana”` を返す
3. `”orange”` は `”apple” | “banana”` に含まれるか? → いいえ → `never` を返す
4. `”grape”` は `”apple” | “banana”` に含まれるか? → いいえ → `never` を返す

結果として、`Filter` は、以下のようになります。

`”apple” | “banana” | never | never`

ここで、不思議なことが起こります。TypeScriptの世界では、`never` 型は 「何にも含まれない」 という特殊な存在なので、ユニオン型に `never` が含まれていても、それは無視されるんです!

つまり、`”apple” | “banana” | never | never` は、最終的に…

`”apple” | “banana”`

となるんです!

やったー!まるで、お菓子の袋から「もう食べたくない味」だけをそっと取り除いて、美味しい味だけを残せたみたいですね!

実践!コードで見てみよう!

では、実際にコードを書いて、この `never` を使ったフィルタリングを体験してみましょう。
エラーを発生させる関数や、処理を終了させる関数を例にすると、より `never` のイメージが掴みやすいかもしれません。

例1:特定の文字列型だけを除外する

// 型のユニオン
type AllStatus = “pending” | “processing” | “success” | “error”;

// “success” と “error” 以外のステータスだけを残したい場合
// Filter は、T の各要素が U に含まれるかをチェックする
// 含まれれば T を返し、含まれなければ never を返す
type Filter = T extends U ? T : never;

// “success” または “error” に合致しないものをフィルタリング
// “pending” は “success” | “error” に含まれない → never
// “processing” は “success” | “error” に含まれない → never
// “success” は “success” | “error” に含まれる → “success”
// “error” は “success” | “error” に含まれる → “error”
// 結果として “never | never | success | error” となり、never は無視されるので “success | error” になる
type SuccessOrErrorStatus = Filter;

// この型は “success” | “error” となります。
// 確認してみましょう:
let myStatus: SuccessOrErrorStatus;
myStatus = “success”; // OK
myStatus = “error”; // OK
// myStatus = “pending”; // エラー! “pending” はこの型には含まれません。

console.log(“SuccessOrErrorStatus:”, “success” extends SuccessOrErrorStatus ? true : false); // true
console.log(“SuccessOrErrorStatus:”, “error” extends SuccessOrErrorStatus ? true : false); // true
console.log(“SuccessOrErrorStatus:”, “pending” extends SuccessOrErrorStatus ? true : false); // false

例2:関数からの返り値の型を絞り込む

たとえば、ある処理が成功したら値を返すけれど、失敗したらエラーを投げて処理を中断するような関数があるとします。

// 成功時の値の型
type SuccessResult = { status: “success”; data: string };
// 失敗時の値の型(実際にはこの型が返ることはない)
type FailureResult = { status: “error”; message: string };

// 処理を実行する関数
function processData(): SuccessResult | FailureResult {
// 何らかの処理…
const isSuccess = Math.random() > 0.5; // 50%の確率で成功とする

if (isSuccess) {
return { status: “success”, data: “処理が成功しました!” };
} else {
// 失敗した場合は、エラーを投げて処理を終了させる
// throw new Error(“処理中にエラーが発生しました。”);
// never 型は、関数から決して値を返さないことを示すために使われることもあります。
// ここで never を返すわけではありませんが、概念として「この後、値は来ない」というイメージです。
return { status: “error”, message: “処理中にエラーが発生しました。” };
}
}

// 関数を実行した結果の型
const result = processData();

// Conditional Types と never を使って、成功時のデータだけを取り出したい場合
// T が U に含まれるかをチェック
// ここでは、result の型が { status: “success”; data: string } という条件に合致するかどうかで分岐
type GetSuccessData = T extends { status: “success”; data: infer D } ? D : never;

// GetSuccessData は、result が { status: “success”; data: string } の形であれば string を返す。
// そうでなければ never を返す。
type SuccessDataType = GetSuccessData;

// もし result が “success” の形であれば、SuccessDataType は string になる。
// そうでなければ never になる。
// TypeScript は typeof result を見て、成功する可能性も失敗する可能性もあると判断する。
// そのため、Safe に data にアクセスするには、型ガードが必要になる。

if (result.status === “success”) {
// ここでは result.status が “success” であることが保証されているため、
// result.data に安全にアクセスできる。
const data: string = result.data; // SuccessDataType は string と推論される
console.log(“成功データ:”, data);
} else {
// ここでは result.status が “error” であることが保証されている。
console.log(“エラーメッセージ:”, result.message);
}

// GetSuccessData を使った例(型ガードと組み合わせる)
// result が成功時の型である場合にのみ、data を取得する
const extractedData: SuccessDataType = (result.status === “success”) ? result.data : undefined as any; // 型システム上は never になる可能性もあるため、型アサーションが必要になる場合も
// もし result が常に成功することを保証できるなら、以下のように書ける
// const extractedData: SuccessDataType = result.data;

// ここで、SuccessDataType が “success” の場合に string、それ以外は never になることを確認
console.log(“SuccessDataType:”, extractedData); // result が成功した場合のみ string が表示される

※注意点: 上記の例2では、`processData` の返り値の型が `SuccessResult | FailureResult` であるため、`GetSuccessData` は、`result` が `SuccessResult` の型である可能性があれば `string` を、そうでなければ `never` を返します。しかし、`result` 自体は実行時にどちらかの型になるため、`data` に安全にアクセスするには、やはり `if (result.status === “success”)` のような型ガードが不可欠です。 `never` を使うのは、あくまで「もしこの条件に合致しなかったら、それはもう存在しない型だよ」ということを型システムに伝えるためなんです。

まとめ:neverは「不要なもの」を静かに消してくれる縁の下の力持ち

Conditional Typesと `never` 型を組み合わせることで、ユニオン型の中から不要な型をスマートに除外できることがお分かりいただけたでしょうか?

まるで、お菓子の袋から苦手な味だけを取り除いたり、必要な道具だけを厳選したりするような感覚です。

  • `never`: 「この型は、もうここには存在しない」「決して到達しない場所」
  • Conditional Types: 「もし~なら、こうする。そうでなければ、こうする。」という条件分岐
  • 組み合わせ: 「もし、この条件に合致しない型があったら、それは `never`(存在しないもの)として扱おう!」

という流れで、型をスッキリ整理してくれるんです。

最初は少し難しく感じるかもしれませんが、このテクニックを使いこなせるようになると、より複雑な型定義もスッキリと、そして安全に書けるようになります。
ぜひ、あなたのTypeScriptライフで、この `never` 型の裏技を試してみてくださいね!

もし、この辺りで「あれ?ここがよくわからないかも…」という部分があれば、遠慮なく質問してください。あなたの「なるほど!」を引き出すお手伝いができたら嬉しいです!

コメント

タイトルとURLをコピーしました