はじめてのTypeScriptプログラミング入門 30日コース | TypeScriptの核心「型」 - Day 18:型の絞り込み

30日で身につけるTypeScript
スポンサーリンク
スポンサーリンク
  1. Day 18:型の絞り込み(Type Narrowing)で「安全に分岐する」感覚を身につける
  2. 型の絞り込みとは:ざっくり「今はこの型だ」と確信していくこと
    1. Union Typeのままだと「どっちかわからない」
  3. typeofでプリミティブ型を絞り込む
    1. 実践例:printValue関数
    2. 型の絞り込みがどう効いているか
  4. printValue関数を少しアレンジしてみる
    1. 実行例を追加する
  5. instanceofでオブジェクト型を絞り込む
    1. クラスを使った例
  6. inでプロパティの有無から型を絞り込む
    1. プロパティの存在チェックで絞り込む
  7. Union Typeとの組み合わせで「賢い分岐」を書く
    1. 文字列と数値の両方を処理する関数をもう一歩発展させる
  8. Day 18 練習テンプレート:型の絞り込みをまとめて体験する
  9. Day 18のまとめ
  10. Day 18:型の絞り込み演習で「安全な判定ロジック」を身につける
  11. String / Number判定:typeofでプリミティブ型を絞り込む
    1. なぜ判定が必要になるのか
    2. typeofでString / Numberを判定する
  12. User / Admin判定:instanceofとUnionで「役割」を絞り込む
    1. クラスでUserとAdminを表現する
    2. Union + Literal Typeで「role」を判定するパターン
  13. 商品 / ユーザー判定:inでプロパティの有無から絞り込む
    1. ProductとUserの型を用意する
    2. inで「どのプロパティを持っているか」を見る
  14. Day 18 演習テンプレート:3つの判定をまとめて練習する
  15. Day 18のまとめ

Day 18:型の絞り込み(Type Narrowing)で「安全に分岐する」感覚を身につける

第3週は、TypeScriptの核心である「型」をじっくり育てていく週です。 Day 18のテーマは 型の絞り込み(Type Narrowing)。 Union Typeと組み合わせて、「状況に応じて型を狭くしていく」考え方を身につけていきます。

学習内容は次のとおりです。

  • typeof
  • instanceof
  • in
  • Type Narrowing
  • Unionとの組み合わせ

実践として、文字列と数値の両方を処理する関数を題材に、型の絞り込みを体験していきます。

型の絞り込みとは:ざっくり「今はこの型だ」と確信していくこと

Union Typeのままだと「どっちかわからない」

まず、次のようなUnion Typeを考えます。

// 文字列か数値のどちらか
type StringOrNumber = string | number;
TypeScript

この型を引数に持つ関数を作ると、こうなります。

function printValue(value: string | number) {
  // ここでは value が string か number か、まだわからない
}
TypeScript

value の型は string | number なので、 TypeScriptから見て「文字列か数値か、どちらの可能性もある」という状態です。

このままでは、value.toUpperCase() も value.toFixed(2) も、 「どちらかの型にしか存在しないメソッド」なので、素直には呼べません。

ここで必要になるのが 型の絞り込み(Type Narrowing)です。

typeofでプリミティブ型を絞り込む

実践例:printValue関数

まずは、提示されている関数をじっくり見てみます。

function printValue(value: string | number) {
  if (typeof value === "string") {
    console.log(value.toUpperCase());
  } else {
    console.log(value.toFixed(2));
  }
}
TypeScript

この関数は、次のような動きをします。

  • 引数 value は string | number(文字列か数値のどちらか)
  • typeof value === "string" のときは、文字列として扱う
  • それ以外(ここでは number)のときは、数値として扱う

型の絞り込みがどう効いているか

if (typeof value === "string") のブロックの中では、 TypeScriptは「この中では value は string だ」と理解します。

if (typeof value === "string") {
  // ここでは value は string 型に絞り込まれている
  console.log(value.toUpperCase()); // stringのメソッドが使える
} else {
  // ここでは value は number 型に絞り込まれている
  console.log(value.toFixed(2));    // numberのメソッドが使える
}
TypeScript

重要ポイント(ここはしっかり深掘りしたいところです):

  • Union Type(string | number)のままだと、「どちらのメソッドも安全に呼べない」状態です。
  • typeof を使って条件分岐することで、「このブロックの中ではstring」「このブロックの中ではnumber」と、型を狭くできます。
  • これが Type Narrowing(型の絞り込み)であり、TypeScriptが「文脈に応じて型を理解してくれる」仕組みです。

printValue関数を少しアレンジしてみる

実行例を追加する

実際にどう動くか、テストコードを足してみます。

function printValue(value: string | number) {
  if (typeof value === "string") {
    // ここでは value は string 型
    console.log("文字列として処理:", value.toUpperCase());
  } else {
    // ここでは value は number 型
    console.log("数値として処理:", value.toFixed(2));
  }
}

// テスト
printValue("hello"); // 文字列として処理: HELLO
printValue(3.14159); // 数値として処理: 3.14
TypeScript

コメントを付けることで、「どのタイミングで型が絞り込まれているか」が視覚的にもわかりやすくなります。

instanceofでオブジェクト型を絞り込む

クラスを使った例

typeof は主にプリミティブ型(string, number, boolean など)に使います。 オブジェクトやクラスのインスタンスを絞り込みたいときは、instanceof を使います。

class Person {
  constructor(public name: string) {}
}

class Animal {
  constructor(public species: string) {}
}

function printEntity(entity: Person | Animal) {
  if (entity instanceof Person) {
    // ここでは entity は Person 型
    console.log("人の名前:", entity.name);
  } else {
    // ここでは entity は Animal 型
    console.log("動物の種類:", entity.species);
  }
}
TypeScript

ポイント:

  • entity instanceof Person のブロックでは、entity が Person 型に絞り込まれます。
  • それ以外のブロックでは、Animal 型に絞り込まれます。
  • クラスベースのオブジェクトを扱うときに、instanceof はよく使われます。

inでプロパティの有無から型を絞り込む

プロパティの存在チェックで絞り込む

in 演算子は、「オブジェクトが特定のプロパティを持っているかどうか」を調べるものです。 これを使って、Union Typeの中から「このプロパティを持つ型」を絞り込むことができます。

type User = {
  name: string;
  email: string;
};

type Guest = {
  name: string;
};

// UserかGuestのどちらか
type UserOrGuest = User | Guest;

function printUserInfo(person: UserOrGuest) {
  if ("email" in person) {
    // ここでは person は User 型に絞り込まれている
    console.log("ユーザーのメール:", person.email);
  } else {
    // ここでは person は Guest 型に絞り込まれている
    console.log("ゲストユーザー:", person.name);
  }
}
TypeScript

重要ポイント:

  • if ("email" in person) のブロックでは、「emailプロパティを持つ型」だけに絞り込まれます。
  • Union Typeのメンバーが「持っているプロパティの違い」で区別できるときに、とても便利なパターンです。

Union Typeとの組み合わせで「賢い分岐」を書く

文字列と数値の両方を処理する関数をもう一歩発展させる

先ほどの printValue 関数を、少しだけ拡張してみます。 例えば、「数値のときは丸め方を変えたい」といったオプションを追加してみます。

// 丸め方のオプション
type RoundMode = "ceil" | "floor" | "round";

// 文字列と数値の両方を処理する関数(オプション付き)
function printValueAdvanced(value: string | number, mode: RoundMode = "round") {
  if (typeof value === "string") {
    // 文字列の場合は大文字にして表示
    console.log("文字列:", value.toUpperCase());
  } else {
    // 数値の場合は mode に応じて丸め方を変える
    let processed: number;

    switch (mode) {
      case "ceil":
        processed = Math.ceil(value);
        break;
      case "floor":
        processed = Math.floor(value);
        break;
      case "round":
      default:
        processed = Math.round(value);
        break;
    }

    console.log("数値:", processed.toFixed(2));
  }
}

// テスト
printValueAdvanced("hello");          // 文字列: HELLO
printValueAdvanced(3.14159);          // 数値: 3.00(round)
printValueAdvanced(3.14159, "ceil");  // 数値: 4.00
printValueAdvanced(3.14159, "floor"); // 数値: 3.00
TypeScript

ここでも、

  • value は string | number のUnion Type
  • mode は "ceil" | "floor" | "round" のUnion Type(Literal Typeの組み合わせ)
  • typeof と switch を使って、それぞれの型に応じた処理を安全に書いています。

Day 18 練習テンプレート:型の絞り込みをまとめて体験する

最後に、今日の内容をひとつのファイルにまとめた練習テンプレートを載せます。 このままコピーして、条件分岐を増やしたり、型を変えたりしながら、自分なりの「型の絞り込み」を試してみてください。

// day18-type-narrowing.ts
// Day 18:型の絞り込み 練習テンプレート

// 1. typeof を使った文字列と数値の絞り込み
function printValue(value: string | number) {
  if (typeof value === "string") {
    // ここでは value は string 型
    console.log("文字列として処理:", value.toUpperCase());
  } else {
    // ここでは value は number 型
    console.log("数値として処理:", value.toFixed(2));
  }
}

// 2. instanceof を使ったクラスインスタンスの絞り込み
class Person {
  constructor(public name: string) {}
}

class Animal {
  constructor(public species: string) {}
}

function printEntity(entity: Person | Animal) {
  if (entity instanceof Person) {
    console.log("人の名前:", entity.name);
  } else {
    console.log("動物の種類:", entity.species);
  }
}

// 3. in を使ったプロパティによる絞り込み
type User = {
  name: string;
  email: string;
};

type Guest = {
  name: string;
};

type UserOrGuest = User | Guest;

function printUserInfo(person: UserOrGuest) {
  if ("email" in person) {
    console.log("ユーザーのメール:", person.email);
  } else {
    console.log("ゲストユーザー:", person.name);
  }
}

// 4. Union Type + Narrowing を組み合わせた発展例
type RoundMode = "ceil" | "floor" | "round";

function printValueAdvanced(value: string | number, mode: RoundMode = "round") {
  if (typeof value === "string") {
    console.log("文字列:", value.toUpperCase());
  } else {
    let processed: number;

    switch (mode) {
      case "ceil":
        processed = Math.ceil(value);
        break;
      case "floor":
        processed = Math.floor(value);
        break;
      case "round":
      default:
        processed = Math.round(value);
        break;
    }

    console.log("数値:", processed.toFixed(2));
  }
}

// テスト
printValue("hello");
printValue(3.14159);

printEntity(new Person("Taro"));
printEntity(new Animal("Cat"));

printUserInfo({ name: "Guest", email: "[email protected]" });
printUserInfo({ name: "Anonymous" });

printValueAdvanced("typescript");
printValueAdvanced(3.14159);
printValueAdvanced(3.14159, "ceil");
printValueAdvanced(3.14159, "floor");
TypeScript

Day 18のまとめ

Day 18では、

  • Union Type(string | number など)をそのまま使うと「どっちかわからない」状態になること
  • typeof / instanceof / in を使って、文脈に応じて型を狭くしていく Type Narrowing の考え方
  • 文字列と数値の両方を処理する printValue 関数を通して、「絞り込み後の型に応じたメソッドを安全に呼ぶ」流れ
  • クラスインスタンスやプロパティの有無による絞り込みなど、Union Typeとの組み合わせパターン

を、具体的なコードとともに体験しました。

型の絞り込みは、TypeScriptが「コードの流れを理解してくれる」仕組みの中心的な機能です。 ぜひ、条件分岐を増やしたり、Union Typeのパターンを変えたりしながら、「この場面ではこの型だ」と確信していく感覚を育ててみてください。そうすると、型が味方になってくれて、安心して複雑なロジックを書けるようになっていきます。


Day 18:型の絞り込み演習で「安全な判定ロジック」を身につける

第3週は、TypeScriptの核心である「型」をじっくり育てていく週です。 Day 18の演習テーマは、型の絞り込み(Type Narrowing)を使った3つの判定ロジックです。

  • String / Number判定(typeof)
  • User / Admin判定(instanceof や Unionとの組み合わせ)
  • 商品 / ユーザー判定(in)

学習内容は次のとおりです。

  • typeof
  • instanceof
  • in
  • Type Narrowing
  • Unionとの組み合わせ

ここからは、それぞれをステップバイステップで見ていきます。

String / Number判定:typeofでプリミティブ型を絞り込む

なぜ判定が必要になるのか

まずは、「文字列か数値か、どちらかが来る」関数を考えます。

function printValue(value: string | number) {
  // ここでは value が string か number か、まだわからない
}
TypeScript

value の型は string | number なので、TypeScriptから見て「文字列か数値か、どちらの可能性もある」という状態です。 このままでは、value.toUpperCase() も value.toFixed(2) も安全に呼べません。

そこで使うのが typeof による型の絞り込みです。

typeofでString / Numberを判定する

function printValue(value: string | number) {
  if (typeof value === "string") {
    // ここでは value は string 型に絞り込まれている
    console.log("文字列として処理:", value.toUpperCase());
  } else {
    // ここでは value は number 型に絞り込まれている
    console.log("数値として処理:", value.toFixed(2));
  }
}

// テスト
printValue("hello");   // 文字列として処理: HELLO
printValue(3.14159);   // 数値として処理: 3.14
TypeScript

重要ポイント:

  • typeof value === "string" のブロックの中では、TypeScriptは「ここではvalueはstringだ」と理解します。
  • それ以外のブロックでは、「ここではvalueはnumberだ」と理解します。
  • Union Type(string | number)を「文脈に応じて狭くする」ことが、Type Narrowingの本質です。

User / Admin判定:instanceofとUnionで「役割」を絞り込む

クラスでUserとAdminを表現する

次に、UserとAdminをクラスで表現して判定するパターンを見ていきます。

class User {
  constructor(public name: string) {}
}

class Admin {
  constructor(public name: string, public permissions: string[]) {}
}
TypeScript

ここで、「UserかAdminのどちらかが渡される」関数を作ります。

function printPersonInfo(person: User | Admin) {
  if (person instanceof Admin) {
    // ここでは person は Admin 型に絞り込まれている
    console.log("管理者:", person.name);
    console.log("権限一覧:", person.permissions.join(", "));
  } else {
    // ここでは person は User 型に絞り込まれている
    console.log("一般ユーザー:", person.name);
  }
}

// テスト
const user = new User("Taro");
const admin = new Admin("Admin Hanako", ["manage_users", "view_logs"]);

printPersonInfo(user);
printPersonInfo(admin);
TypeScript

深掘りポイント:

  • person instanceof Admin のブロックでは、「Adminクラスのインスタンスだけ」に絞り込まれます。
  • それ以外のブロックでは、「Userクラスのインスタンス」に絞り込まれます。
  • クラスベースのオブジェクトを扱うときに、instanceof はとても自然な絞り込み方法です。

Union + Literal Typeで「role」を判定するパターン

クラスを使わずに、roleプロパティでUser / Adminを判定することもできます。

type Role = "user" | "admin";

type BaseUser = {
  name: string;
  role: Role;
};

type AdminUser = BaseUser & {
  role: "admin";
  permissions: string[];
};

type NormalUser = BaseUser & {
  role: "user";
};

type AnyUser = AdminUser | NormalUser;

function printUserRoleInfo(user: AnyUser) {
  if (user.role === "admin") {
    // ここでは user は AdminUser 型に絞り込まれている
    console.log("管理者ユーザー:", user.name);
    console.log("権限:", user.permissions.join(", "));
  } else {
    // ここでは user は NormalUser 型に絞り込まれている
    console.log("一般ユーザー:", user.name);
  }
}

// テスト
const adminUser: AdminUser = {
  name: "Admin Taro",
  role: "admin",
  permissions: ["manage_users", "view_logs"],
};

const normalUser: NormalUser = {
  name: "User Hanako",
  role: "user",
};

printUserRoleInfo(adminUser);
printUserRoleInfo(normalUser);
TypeScript

ポイント:

  • user.role === "admin" のブロックでは、「roleがadminの型」に絞り込まれます。
  • Literal Type("admin" | "user")とUnion Typeを組み合わせることで、「ありえる役割」を型で表現できます。
  • セキュリティ的にも、「存在しない権限」を扱わないようにするための重要なパターンです。

商品 / ユーザー判定:inでプロパティの有無から絞り込む

ProductとUserの型を用意する

最後に、商品とユーザーを同じ関数で扱い、どちらかを判定するパターンを見ていきます。

type Product = {
  id: number;
  name: string;
  price: number;
};

type UserInfo = {
  id: number;
  name: string;
  email: string;
};

// 商品かユーザーのどちらか
type ProductOrUser = Product | UserInfo;
TypeScript

inで「どのプロパティを持っているか」を見る

ここで、in 演算子を使って「emailプロパティの有無」で判定します。

function printEntityInfo(entity: ProductOrUser) {
  if ("email" in entity) {
    // ここでは entity は UserInfo 型に絞り込まれている
    console.log("ユーザー情報:");
    console.log("ID:", entity.id);
    console.log("名前:", entity.name);
    console.log("メール:", entity.email);
  } else {
    // ここでは entity は Product 型に絞り込まれている
    console.log("商品情報:");
    console.log("ID:", entity.id);
    console.log("商品名:", entity.name);
    console.log("価格:", entity.price);
  }
}

// テスト
const product: Product = {
  id: 100,
  name: "ノートPC",
  price: 120000,
};

const userInfo: UserInfo = {
  id: 1,
  name: "Taro",
  email: "[email protected]",
};

printEntityInfo(product);
printEntityInfo(userInfo);
TypeScript

重要ポイント:

  • if ("email" in entity) のブロックでは、「emailプロパティを持つ型」だけに絞り込まれます。
  • Union Typeのメンバーが「持っているプロパティの違い」で区別できるときに、とても便利なパターンです。
  • 実際のアプリでも、「レスポンスがユーザーか商品かで形が違う」といった場面でよく使われます。

Day 18 演習テンプレート:3つの判定をまとめて練習する

最後に、今日の3つの判定パターンをひとつのファイルにまとめた練習テンプレートを載せます。 このままコピーして、条件分岐を増やしたり、型を変えたりしながら、自分なりの「型の絞り込み」を試してみてください。

// day18-narrowing-practice.ts
// Day 18:型の絞り込み 演習テンプレート

// 1. String / Number判定(typeof)
function printValue(value: string | number) {
  if (typeof value === "string") {
    console.log("文字列として処理:", value.toUpperCase());
  } else {
    console.log("数値として処理:", value.toFixed(2));
  }
}

printValue("hello");
printValue(3.14159);

// 2. User / Admin判定(instanceof)
class User {
  constructor(public name: string) {}
}

class Admin {
  constructor(public name: string, public permissions: string[]) {}
}

function printPersonInfo(person: User | Admin) {
  if (person instanceof Admin) {
    console.log("管理者:", person.name);
    console.log("権限一覧:", person.permissions.join(", "));
  } else {
    console.log("一般ユーザー:", person.name);
  }
}

const user = new User("Taro");
const admin = new Admin("Admin Hanako", ["manage_users", "view_logs"]);

printPersonInfo(user);
printPersonInfo(admin);

// 2-2. User / Admin判定(roleによるUnion)
type Role = "user" | "admin";

type BaseUser = {
  name: string;
  role: Role;
};

type AdminUser = BaseUser & {
  role: "admin";
  permissions: string[];
};

type NormalUser = BaseUser & {
  role: "user";
};

type AnyUser = AdminUser | NormalUser;

function printUserRoleInfo(user: AnyUser) {
  if (user.role === "admin") {
    console.log("管理者ユーザー:", user.name);
    console.log("権限:", user.permissions.join(", "));
  } else {
    console.log("一般ユーザー:", user.name);
  }
}

const adminUser: AdminUser = {
  name: "Admin Taro",
  role: "admin",
  permissions: ["manage_users", "view_logs"],
};

const normalUser: NormalUser = {
  name: "User Hanako",
  role: "user",
};

printUserRoleInfo(adminUser);
printUserRoleInfo(normalUser);

// 3. 商品 / ユーザー判定(in)
type Product = {
  id: number;
  name: string;
  price: number;
};

type UserInfo = {
  id: number;
  name: string;
  email: string;
};

type ProductOrUser = Product | UserInfo;

function printEntityInfo(entity: ProductOrUser) {
  if ("email" in entity) {
    console.log("ユーザー情報:");
    console.log("ID:", entity.id);
    console.log("名前:", entity.name);
    console.log("メール:", entity.email);
  } else {
    console.log("商品情報:");
    console.log("ID:", entity.id);
    console.log("商品名:", entity.name);
    console.log("価格:", entity.price);
  }
}

const productEntity: Product = {
  id: 100,
  name: "ノートPC",
  price: 120000,
};

const userEntity: UserInfo = {
  id: 1,
  name: "Taro",
  email: "[email protected]",
};

printEntityInfo(productEntity);
printEntityInfo(userEntity);
TypeScript

Day 18のまとめ

Day 18では、

  • typeof を使って、String / Numberを安全に判定する方法
  • instanceof や Literal Type + Unionを使って、User / Adminを判定するパターン
  • in を使って、商品 / ユーザーを「プロパティの違い」で判定する方法
  • それぞれの場面で、Type Narrowingによって「このブロックではこの型だ」と確信してコードを書く流れ

を、具体的なコードとともに体験しました。

型の絞り込みは、TypeScriptが「コードの流れを理解してくれる」仕組みの中心的な機能です。 ぜひ、条件分岐を増やしたり、Union Typeのパターンを変えたりしながら、「この場面ではこの型だ」と確信していく感覚を育ててみてください。そうすると、型が味方になってくれて、安心して複雑なロジックを書けるようになっていきます。

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