メインコンテンツまでスキップ

型の絞り込み

この章では、TypeScriptの型システムの中核となる「型の絞り込み(Type Narrowing)」について学びます。

この章で学ぶこと

  • Union型とIntersection型の使い方と違い
  • 型ガード(typeof, in, instanceof)による型の絞り込み
  • タグ付きUnion(Discriminated Union)
  • カスタム型ガード関数
  • 型アサーション(as)と非nullアサーション(!)
備考

この章の内容は、TypeScript Playgroundでも確認できます。実際に手を動かしながら学習を進めましょう。

Union型(ユニオン型)

Union型とは

Union型は、複数の型のうちいずれかの型を表す型です。|(パイプ)記号を使って定義します。

// Union型の基本
// string型 または number型 を代入できる変数
let value: string | number;

value = "hello"; // OK: string型
value = 100; // OK: number型
// value = true; // Error: boolean型は許可されていない

Union型と関数

// Union型を引数に取る関数
// idはstring型またはnumber型を受け取る
function printId(id: string | number): void {
console.log(`Your ID is: ${id}`);
}

printId(101); // OK
printId("ABC123"); // OK
// printId(true); // Error: boolean型は許可されていない

Union型の制限と対処

Union型の変数にアクセスできるのは、すべての型に共通するメンバーのみです。

function printLength(value: string | number): void {
// Error: number型にはlengthプロパティがない
// console.log(value.length);

// 解決策: 型ガードで型を絞り込む
if (typeof value === "string") {
// この分岐内ではvalueはstring型として扱われる
console.log(value.length); // OK
} else {
console.log("Not a string");
}
}

配列のUnion型

// 配列要素がstring型またはnumber型
// 各要素はstringでもnumberでもOK
let mixedArray: (string | number)[];
mixedArray = [1, "two", 3, "four"]; // OK

// 「string型の配列」または「number型の配列」
// 配列全体がどちらか一方の型
let arrayUnion: string[] | number[];
arrayUnion = ["a", "b", "c"]; // OK: string[]
arrayUnion = [1, 2, 3]; // OK: number[]
// arrayUnion = [1, "a"]; // Error: 混在は不可

Intersection型(インターセクション型)

Intersection型とは

Intersection型は、複数の型をすべて満たす型です。&(アンパサンド)記号を使って定義します。

// 2つの型を定義
type Person = {
name: string;
age: number;
};

type Employee = {
employeeId: string;
department: string;
};

// Intersection型で結合
// PersonとEmployeeの両方のプロパティを持つ
type Staff = Person & Employee;

const staff: Staff = {
name: "田中太郎",
age: 30,
employeeId: "EMP001",
department: "開発部"
};
// 1つでもプロパティが欠けるとエラー

実践的な例:機能の組み合わせ

// 各機能を個別の型として定義
type Readable = {
read(): void;
};

type Writable = {
write(data: string): void;
};

type Closable = {
close(): void;
};

// 機能を組み合わせた型
// すべてのメソッドを持つ必要がある
type FileHandle = Readable & Writable & Closable;

// クラスでFileHandle型を実装
class TextFile implements FileHandle {
read(): void {
console.log("ファイルを読み込みます");
}

write(data: string): void {
console.log(`データを書き込みます: ${data}`);
}

close(): void {
console.log("ファイルを閉じます");
}
}

Union型とIntersection型の違い

// Union型: いずれかの型(OR)
type StringOrNumber = string | number;
// → stringでもnumberでもOK

// Intersection型: すべての型を満たす(AND)
type StringAndNumber = string & number;
// → stringかつnumber → never型(存在しない)

// オブジェクト型ではIntersectionが有効
type Combined = { a: string } & { b: number };
// → { a: string; b: number } 両方のプロパティを持つ

型ガード(Type Guard)

型ガードは、条件分岐を使って型を絞り込む手法です。

typeof型ガード

function processValue(value: string | number): string {
// typeof演算子で型をチェック
if (typeof value === "string") {
// この中ではvalueはstring型として扱われる
return value.toUpperCase();
} else {
// この中ではvalueはnumber型として扱われる
return value.toFixed(2);
}
}

console.log(processValue("hello")); // "HELLO"
console.log(processValue(123.456)); // "123.46"

typeof演算子で判定できる型:

判定結果対象の型
"string"文字列
"number"数値
"boolean"真偽値
"object"オブジェクト(nullも含む)
"function"関数
"undefined"undefined
"symbol"シンボル
"bigint"BigInt
備考

配列の判定は Array.isArray を使う

typeof では配列を判別できません(配列も "object" を返します)。配列かどうかをチェックしたい場合は Array.isArray() を使います。TypeScript はこれを型ガードとして認識するため、if (Array.isArray(x)) の中では自動的に x が配列型に絞り込まれます。

function processValue(value: string | string[]): number {
if (Array.isArray(value)) {
return value.length // value は string[]
}
return value.length // value は string
}

in演算子による型ガード

オブジェクトにプロパティが存在するかチェックします。

type Dog = {
name: string;
bark(): void;
};

type Cat = {
name: string;
meow(): void;
};

type Pet = Dog | Cat;

function makeSound(pet: Pet): void {
// nameプロパティは両方にあるので直接アクセス可
console.log(`${pet.name}が鳴きます:`);

// in演算子でプロパティの存在をチェック
if ("bark" in pet) {
// barkプロパティがあるのでpetはDog型
pet.bark();
} else {
// barkがないのでpetはCat型
pet.meow();
}
}

const dog: Dog = {
name: "ポチ",
bark() { console.log("ワンワン!"); }
};

const cat: Cat = {
name: "タマ",
meow() { console.log("ニャー!"); }
};

makeSound(dog); // "ポチが鳴きます: ワンワン!"
makeSound(cat); // "タマが鳴きます: ニャー!"

instanceof型ガード

クラスのインスタンスかどうかを判定します。

class Bird {
fly(): void {
console.log("空を飛びます");
}

sing(): void {
console.log("さえずります");
}
}

class Fish {
swim(): void {
console.log("泳ぎます");
}
}

type Animal = Bird | Fish;

function move(animal: Animal): void {
// instanceof演算子でクラスのインスタンスかチェック
if (animal instanceof Bird) {
// animalはBird型として扱われる
animal.fly();
animal.sing();
} else {
// animalはFish型として扱われる
animal.swim();
}
}

const bird = new Bird();
const fish = new Fish();

move(bird); // "空を飛びます" "さえずります"
move(fish); // "泳ぎます"

タグ付きUnion(Discriminated Union)

リテラル型をタグとして使用することで、型を安全に判別できます。

// statusプロパティがリテラル型のタグ
type Success = {
status: "success"; // リテラル型 "success"
data: string;
};

type Failure = {
status: "failure"; // リテラル型 "failure"
error: string;
};

type Result = Success | Failure;

function handleResult(result: Result): void {
// statusプロパティの値で型を判別
if (result.status === "success") {
// resultはSuccess型として扱われる
console.log(`成功: ${result.data}`);
} else {
// resultはFailure型として扱われる
console.log(`失敗: ${result.error}`);
}
}

// switch文でも使用可能
function handleResultSwitch(result: Result): void {
switch (result.status) {
case "success":
console.log(`成功: ${result.data}`);
break;
case "failure":
console.log(`失敗: ${result.error}`);
break;
}
}

const success: Success = { status: "success", data: "処理完了" };
const failure: Failure = { status: "failure", error: "エラーが発生しました" };

handleResult(success); // "成功: 処理完了"
handleResult(failure); // "失敗: エラーが発生しました"

実践例:APIレスポンスの状態管理

// 読み込み中
type LoadingState = {
status: "loading";
};

// 成功
type SuccessState<T> = {
status: "success";
data: T;
};

// エラー
type ErrorState = {
status: "error";
error: string;
};

// 3つの状態のいずれか
type ApiState<T> = LoadingState | SuccessState<T> | ErrorState;

// ユーザー情報の型
type User = {
id: number;
name: string;
};

function renderUI(state: ApiState<User>): void {
switch (state.status) {
case "loading":
console.log("読み込み中...");
break;
case "success":
// SuccessState型なのでdataにアクセス可能
console.log(`ユーザー: ${state.data.name}`);
break;
case "error":
// ErrorState型なのでerrorにアクセス可能
console.log(`エラー: ${state.error}`);
break;
}
}

網羅性チェック(exhaustive check)

タグ付き Union と switch 文を組み合わせるとき、すべての case を処理しないとコンパイルエラーになる仕組みを作れます。これを exhaustive check(網羅性チェック) と呼び、never 型を利用します。

type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; size: number }
| { kind: 'rectangle'; width: number; height: number }

function area(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2
case 'square':
return shape.size ** 2
case 'rectangle':
return shape.width * shape.height
default: {
// すべての case を処理すると shape は never 型になる
// 処理漏れがあるとここで型エラー → コンパイル時に検知できる
const _exhaustive: never = shape
throw new Error(`Unhandled shape kind: ${JSON.stringify(_exhaustive)}`)
}
}
}

このパターンの価値は、後から Shape に新しい kind を追加したときにコンパイルエラーで「ここでも case を追加してね」と教えてくれる点にあります。手書きで書くと case の追加漏れに気づきにくいため、タグ付き Union を扱う際は exhaustive check を併用するのが推奨パターンです。

備考

_exhaustive のようなアンダースコア始まりの名前は「型チェックのためだけの変数」であることを示す命名慣習です。この例では _exhaustive をエラーメッセージの組み立てに使用しているため、TypeScript の noUnusedLocals オプション(tsconfig.json)でも未使用エラーにはなりません。チームの規約で別の表現(例: satisfies never)を使うこともあります。

default:
// 変数を定義せずに satisfies でも同等のチェックができる
shape satisfies never
throw new Error(`Unhandled: ${(shape as { kind: string }).kind}`)

カスタム型ガード関数

複雑な型判定ロジックを再利用可能な関数にできます。

// 型述語(Type Predicate)を使った型ガード関数
// 戻り値の型 "value is string" がポイント
function isString(value: unknown): value is string {
return typeof value === "string";
}

function isNumber(value: unknown): value is number {
return typeof value === "number";
}

function processUnknown(value: unknown): void {
// カスタム型ガード関数で型を絞り込む
if (isString(value)) {
// valueはstring型として扱われる
console.log(value.toUpperCase());
} else if (isNumber(value)) {
// valueはnumber型として扱われる
console.log(value.toFixed(2));
} else {
console.log("文字列でも数値でもありません");
}
}

processUnknown("hello"); // "HELLO"
processUnknown(123.456); // "123.46"
processUnknown(true); // "文字列でも数値でもありません"

オブジェクトの型ガード関数

// ユーザー型の定義
interface User {
id: number;
name: string;
email: string;
}

// Userかどうかを判定する型ガード関数
function isUser(obj: unknown): obj is User {
// nullとオブジェクトかどうかをチェック
if (typeof obj !== "object" || obj === null) {
return false;
}

// 型アサーションで一時的にUserとして扱う
const user = obj as User;

// 必要なプロパティがすべて存在し、正しい型かチェック
return (
typeof user.id === "number" &&
typeof user.name === "string" &&
typeof user.email === "string"
);
}

// 使用例:APIから取得したデータを安全に処理
function greetUser(data: unknown): void {
if (isUser(data)) {
// dataはUser型として扱われる
console.log(`こんにちは、${data.name}さん!`);
console.log(`メール: ${data.email}`);
} else {
console.log("無効なユーザーデータです");
}
}

const validUser = { id: 1, name: "山田太郎", email: "yamada@example.com" };
const invalidData = { id: 1, name: "田中" }; // emailがない

greetUser(validUser); // "こんにちは、山田太郎さん!"
greetUser(invalidData); // "無効なユーザーデータです"

アサーション関数(asserts)

assertsキーワードを使うと、条件を満たさない場合に例外を投げる関数を定義できます。関数が正常に終了した場合、TypeScriptは型を絞り込みます。

// 値がnull/undefinedでないことを保証
function assertIsDefined<T>(value: T): asserts value is NonNullable<T> {
if (value === null || value === undefined) {
throw new Error("Value must be defined");
}
}

function processUser(user: User | null): void {
// user は User | null
assertIsDefined(user);
// assertIsDefinedが例外を投げなかったので、user は User 型に絞り込まれる
console.log(user.name); // OK: user は User 型
}

assertsと型述語の違い

// 型述語(Type Predicate): booleanを返す
function isString(value: unknown): value is string {
return typeof value === "string";
}

// アサーション関数: 例外を投げるか、型を保証する
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error(`Expected string, got ${typeof value}`);
}
}

function example(value: unknown): void {
// 型述語: if文で使う
if (isString(value)) {
console.log(value.toUpperCase()); // OK
}

// アサーション関数: 呼び出し後は型が確定
assertIsString(value);
console.log(value.toUpperCase()); // OK(以降 value は string 型)
}

実践例:バリデーション関数

interface User {
id: number;
name: string;
email: string;
}

function assertIsUser(value: unknown): asserts value is User {
if (typeof value !== "object" || value === null) {
throw new Error("Expected object");
}

const obj = value as Record<string, unknown>;

if (typeof obj.id !== "number") {
throw new Error("Expected id to be number");
}
if (typeof obj.name !== "string") {
throw new Error("Expected name to be string");
}
if (typeof obj.email !== "string") {
throw new Error("Expected email to be string");
}
}

// APIからのレスポンスを安全に処理
async function fetchUser(): Promise<User> {
const response = await fetch("/api/user");
const data: unknown = await response.json();

assertIsUser(data); // データが不正なら例外
return data; // User型として返せる
}

型アサーション(Type Assertion)

TypeScriptコンパイラに「この値は特定の型である」と明示的に伝える機能です。

基本的な型アサーション

// as構文を使用(推奨)
let someValue: unknown = "this is a string";
let strLength: number = (someValue as string).length;

// アングルブラケット構文(JSXでは使用不可)
let someValue2: unknown = "this is a string";
let strLength2: number = (<string>someValue2).length;
警告

型アサーションは型変換ではありません。コンパイル時のみ有効で、実行時には何も起こりません。誤った型アサーションは実行時エラーの原因になるため、使用には注意が必要です。

DOM要素の型アサーション

// DOM要素の取得(返り値はHTMLElement | null)
const inputElement = document.getElementById("username");

// 型アサーションで具体的な型を指定
const input = document.getElementById("username") as HTMLInputElement;

// HTMLInputElementのプロパティにアクセス可能
input.value = "初期値";
input.placeholder = "ユーザー名を入力";

// より安全なアプローチ: 型ガードを使用
const maybeInput = document.getElementById("username");
if (maybeInput instanceof HTMLInputElement) {
maybeInput.value = "初期値"; // 型ガードで安全にアクセス
}

非nullアサーション(!)

値がnullやundefinedでないことを明示します。

// 非nullアサーション演算子(!)
function processValue(value: string | null | undefined): void {
// !をつけることで、nullチェックをスキップ
console.log(value!.toUpperCase());
// 注意: 実際にnullの場合は実行時エラー
}

// より安全なアプローチ: 型ガードを使用
function processValueSafe(value: string | null | undefined): void {
if (value !== null && value !== undefined) {
console.log(value.toUpperCase()); // 型ガードで安全に
}
}
警告

非nullアサーションは使用には注意が必要です。実際にnullの場合、実行時エラーになります。可能な限り型ガードを使用する方が安全です。

試してみよう: APIレスポンスの型ガードを実装しよう ★★

以下のAPIレスポンス型に対して、型安全に処理する関数を実装してください。

要件:

  1. SuccessResponseErrorResponseのタグ付きUnion型を定義
  2. レスポンスを処理する関数を実装
  3. 成功時はデータを、失敗時はエラーメッセージを返す
// 以下の型を使って実装してください
type SuccessResponse = {
status: "success";
data: {
id: number;
message: string;
};
};

type ErrorResponse = {
status: "error";
error: {
code: number;
message: string;
};
};

type ApiResponse = SuccessResponse | ErrorResponse;

// handleResponse関数を実装してください
function handleResponse(response: ApiResponse): string {
// ここに実装
}
ヒント
  1. response.statusの値で型を判別できます
  2. status === "success"の場合、response.dataにアクセスできます
  3. status === "error"の場合、response.errorにアクセスできます
回答と解説
type SuccessResponse = {
status: "success";
data: {
id: number;
message: string;
};
};

type ErrorResponse = {
status: "error";
error: {
code: number;
message: string;
};
};

type ApiResponse = SuccessResponse | ErrorResponse;

// カスタム型ガード関数(オプション)
function isSuccessResponse(response: ApiResponse): response is SuccessResponse {
return response.status === "success";
}

function handleResponse(response: ApiResponse): string {
// タグ付きUnionで型を判別
if (response.status === "success") {
// SuccessResponse型として扱われる
return `成功 (ID: ${response.data.id}): ${response.data.message}`;
} else {
// ErrorResponse型として扱われる
return `エラー (Code: ${response.error.code}): ${response.error.message}`;
}
}

// テスト
const success: SuccessResponse = {
status: "success",
data: { id: 1, message: "データを取得しました" }
};

const error: ErrorResponse = {
status: "error",
error: { code: 404, message: "データが見つかりません" }
};

console.log(handleResponse(success));
// "成功 (ID: 1): データを取得しました"

console.log(handleResponse(error));
// "エラー (Code: 404): データが見つかりません"

解説:

  • statusプロパティがリテラル型"success"または"error"なので、タグ付きUnionとして機能します
  • response.status === "success"のチェック後、TypeScriptはresponseをSuccessResponse型として扱います
  • これにより、response.dataに型安全にアクセスできます

まとめ

この章で学んだこと:

  • Union型|)は「いずれかの型」を表現
  • Intersection型&)は「すべての型を満たす」型を表現
  • 型ガードは条件分岐で型を絞り込む手法
    • typeof: プリミティブ型の判定
    • in: プロパティの存在確認
    • instanceof: クラスのインスタンス判定
  • タグ付きUnionはリテラル型で型を判別するパターン
  • カスタム型ガード関数で複雑な判定を再利用可能に
  • 型アサーション非nullアサーションは使用に注意が必要

次の章では、keyof演算子やLookup型など、さらに高度な型操作について学びます。

初学者がつまずきやすいポイント

警告

よくある間違い

❌ typeof nullが"object"を返すことを知らない

function process(value: string | object | null): void {
if (typeof value === "object") {
// valueはobject | nullになる(nullは除外されない!)
// console.log(value.toString()); // 実行時エラーの可能性
}
}

// ✅ 正しい書き方
function processSafe(value: string | object | null): void {
if (value !== null && typeof value === "object") {
console.log(value.toString()); // OK
}
}

原因: JavaScriptの歴史的な仕様で、typeof null"object"を返します。 解決策: nullチェックを先に行うか、value !== nullを条件に追加しましょう。

❌ 型ガードの結果が分岐外で有効だと思ってしまう

function example(value: string | number): void {
if (typeof value === "string") {
console.log(value.toUpperCase()); // OK
}
// ここではvalueは再びstring | number
// console.log(value.toUpperCase()); // Error
}

原因: 型の絞り込みはその分岐ブロック内でのみ有効です。 解決策: 絞り込んだ後の処理はすべてその分岐内で行いましょう。

❌ カスタム型ガード関数で型述語を忘れる

// ❌ 戻り値がbooleanだと型が絞り込まれない
function isString(value: unknown): boolean {
return typeof value === "string";
}

function process(value: unknown): void {
if (isString(value)) {
// value は依然として unknown
// console.log(value.toUpperCase()); // Error
}
}

// ✅ 型述語(Type Predicate)を使う
function isStringCorrect(value: unknown): value is string {
return typeof value === "string";
}

function processSafe(value: unknown): void {
if (isStringCorrect(value)) {
console.log(value.toUpperCase()); // OK
}
}

原因: 型述語value is stringがないと、TypeScriptは型を絞り込めません。 解決策: カスタム型ガード関数では必ずvalue is Typeの形式で戻り値型を指定しましょう。

❌ 型アサーション(as)を乱用する

// ❌ 型アサーションで強引に型を変換
const input = document.getElementById("input");
const value = (input as HTMLInputElement).value; // inputがnullの場合クラッシュ

// ✅ 型ガードで安全に処理
const inputSafe = document.getElementById("input");
if (inputSafe instanceof HTMLInputElement) {
const value = inputSafe.value; // OK
}

原因: 型アサーションはコンパイル時のみで、実行時チェックがありません。 解決策: 型アサーションより型ガードを優先し、本当に必要な場合のみ使用しましょう。

❌ タグ付きUnionでタグの値を文字列比較する

type Result =
| { status: "success"; data: string }
| { status: "error"; message: string };

// ❌ タイポに気づきにくい
function handle(result: Result): void {
if (result.status === "sucess") { // タイポ!でもエラーにならない可能性
// ...
}
}

// ✅ switch文を使うとタイポがエラーになる
function handleSafe(result: Result): void {
switch (result.status) {
case "success": // 補完が効く、タイポはエラー
console.log(result.data);
break;
case "error":
console.log(result.message);
break;
}
}

原因: if文での文字列比較はタイポに気づきにくいことがあります。 解決策: タグ付きUnionにはswitch文を使うと、補完が効きタイポも防げます。


次に読む