TypeScript | 関数・クラス・ジェネリクス:関数設計の深化 – ユーザー定義型ガード

TypeScript
スポンサーリンク

ゴール:「ユーザー定義型ガード」を“自分で設計して使いこなせる”ようになる

ここで目指したいのは、

「ユーザー定義型ガードって何?」から一歩進んで、
「いつ・どう設計するとコードが楽になるか」までイメージできる状態です。

キーワードはこれです。

  • value is 型 という特別な戻り値型
  • unknown やユニオン型から「安全に絞り込む」
  • 同じ判定ロジックを“関数に閉じ込めて再利用する”

順番に、かみ砕いていきます。


ユーザー定義型ガードとは何か

「自分で作る typeof / Array.isArray みたいなもの」

TypeScript には、すでにいくつか「組み込みの型ガード」があります。

if (typeof value === "string") { /* value は string */ }
if (Array.isArray(value)) { /* value は any[] */ }
TypeScript

これと同じように、

「この関数が true を返したら、引数は〇〇型だとみなしていい」

という“自作の判定関数”を作れる仕組みが
ユーザー定義型ガード(user-defined type guard)です。

そのための文法が、戻り値型の

value is 型
TypeScript

という書き方です。

まずは最小の例:isString

function isString(value: unknown): value is string {
  return typeof value === "string";
}
TypeScript

ここで重要なのは 2 つです。

  • 引数の型:unknown(何が来るか分からない)
  • 戻り値の型:value is string(true のとき string とみなしてよい)

この関数を使うと、こうなります。

function printUpper(value: unknown) {
  if (isString(value)) {
    // ここでは value は string として扱える
    console.log(value.toUpperCase());
  } else {
    console.log("string ではありません");
  }
}
TypeScript

if (isString(value)) の中に入った瞬間、
TypeScript は value の型を string に絞り込んでくれます。

ここが、ユーザー定義型ガードの一番の威力です。


基本形の設計:unknown から安全に絞り込む

ステップ1:判定したい型を決める

例として、Person 型を定義します。

type Person = {
  name: string;
  age: number;
};
TypeScript

外部から来たデータ(JSON など)が Person かどうかを判定したい、
という状況をイメージしてください。

ステップ2:引数は広く(だいたい unknown)

外から来るものは何でも受け取れるように、
引数の型は unknown にするのが定番です。

function isPerson(value: unknown): value is Person {
  // 実装はあとで
}
TypeScript

any ではなく unknown にするのは、
「何も分からないから、ちゃんとチェックしよう」という姿勢を
型で表現するためです。

ステップ3:中身を JavaScript 的にチェックする

Person である条件を、普通の JS で書きます。

function isPerson(value: unknown): value is Person {
  if (typeof value !== "object" || value === null) {
    return false;
  }

  const v = value as { [key: string]: unknown };

  return (
    typeof v.name === "string" &&
    typeof v.age === "number"
  );
}
TypeScript

ここで超重要なのは、

「true を返すときは、本当に Person の条件を満たしていること」

です。

TypeScript は value is Person を「信じるしかない」ので、
ここで嘘をつくと、型安全性が崩れます。

ステップ4:実際に使うとどうなるか

function greet(value: unknown) {
  if (isPerson(value)) {
    // ここでは value は Person 型
    console.log(`Hello, ${value.name} (${value.age})`);
  } else {
    console.log("Person ではありません");
  }
}
TypeScript

if (isPerson(value)) の中では、
valuePerson として扱えるようになっています。

「unknown → Person への安全な橋渡し」を
関数に閉じ込めた、というイメージです。


ユニオン型とユーザー定義型ガード

判別可能ユニオンを“関数で判定する”

次は、ユニオン型の中で「どの種類か」を判定するパターンです。

type Circle = {
  kind: "circle";
  radius: number;
};

type Square = {
  kind: "square";
  size: number;
};

type Shape = Circle | Square;
TypeScript

ShapeCircleSquare かを判定する
ユーザー定義型ガードを作ります。

function isCircle(shape: Shape): shape is Circle {
  return shape.kind === "circle";
}

function isSquare(shape: Shape): shape is Square {
  return shape.kind === "square";
}
TypeScript

使うとどう絞り込まれるか

function area(shape: Shape): number {
  if (isCircle(shape)) {
    // ここでは shape は Circle
    return Math.PI * shape.radius * shape.radius;
  }

  if (isSquare(shape)) {
    // ここでは shape は Square
    return shape.size * shape.size;
  }

  // ここには来ないはず
  throw new Error("Unknown shape");
}
TypeScript

isCircle(shape) が true のブロックでは shapeCircle に、
isSquare(shape) が true のブロックでは shapeSquare
自動で絞り込まれます。

switch (shape.kind) でも同じことはできますが、
ユーザー定義型ガードにすると、

  • 判定ロジックを関数として再利用できる
  • if 文の条件が読みやすくなる(if (isCircle(shape)) は直感的)

というメリットがあります。


配列と相性が良すぎる型ガード

filter に型ガード関数を渡すとどうなるか

ユーザー定義型ガードは、Array.prototype.filter と組み合わせると真価を発揮します。

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

const values: unknown[] = [1, "a", 2, null, 3];

const numbers = values.filter(isNumber);
// numbers: number[]
TypeScript

filter は「型ガード関数が渡されたとき、
戻り値の配列の要素型を絞り込む」という仕様になっています。

つまり、

「配列から“欲しい型だけ”を取り出すとき、
ユーザー定義型ガードを 1 個用意しておくと、
戻り値の型がきれいに狭まる」

ということです。

null / undefined を取り除く汎用型ガード

よく使うパターンをもう 1 つ。

function isNotNull<T>(value: T): value is NonNullable<T> {
  return value !== null && value !== undefined;
}

const arr = [1, null, 2, undefined, 3];

const filtered = arr.filter(isNotNull);
// filtered: number[]
TypeScript

NonNullable<T> は「T から null と undefined を取り除いた型」です。

この isNotNull は、

  • 引数の型:T(何でも)
  • 戻り値の型:value is NonNullable<T>

という形の、ジェネリックなユーザー定義型ガードです。

1 回書いておくと、
「null / undefined を取り除きたい配列」で何度でも使えます。


重要な注意点:「true のときの約束」は絶対に守る

型ガードは TypeScript にとって“信頼するしかない宣言”

戻り値型 value is T は、

「この関数が true を返したとき、value は T だとみなしてよい」

という約束です。

なので、こんな実装は絶対にダメです。

function isNumberBad(value: unknown): value is number {
  return true; // 何でも true にしてしまう
}
TypeScript

これを使うと、

function f(value: unknown) {
  if (isNumberBad(value)) {
    // ここでは value は number とみなされる
    console.log(value.toFixed(2)); // 実行時に落ちるかもしれない
  }
}
TypeScript

TypeScript は「型ガードが true なら number だろう」と信じてくれます。
だからこそ、実装側が嘘をつかないことが超重要です。

逆に、「厳しめに false を返す」のは安全です。

function isPositiveNumber(value: unknown): value is number {
  return typeof value === "number" && value > 0;
}
TypeScript

true のときは「正の number」であることが保証されます。
false のときは「正の number ではない」だけなので、
多少広くても問題ありません。


実務的な設計の視点

どんなときに「ユーザー定義型ガード」に切り出すべきか

こんな状況が出てきたら、型ガードを検討するタイミングです。

  • typeof, in, Array.isArray などのチェックが何度も出てくる
  • API レスポンスの形を毎回手でチェックしている
  • ユニオン型のバリアント判定をあちこちで書いている

同じような判定ロジックが 2 回以上出てきたら、
「これ、ユーザー定義型ガードにできないか?」
と一度立ち止まってみてください。

そうすると、

  • 判定ロジックが 1 箇所にまとまる
  • if 文の条件が読みやすくなる(if (isPerson(value)) など)
  • TypeScript の絞り込みが効いて、補完も気持ちよくなる

という良い循環が生まれます。

名前の付け方も“設計”の一部

型ガード関数は、名前もかなり大事です。

  • isString
  • isPerson
  • isCircle
  • isPositiveNumber
  • isNotNull

など、「is + 〇〇」の形にすると、
if 文を声に出して読んだときに自然になります。

if (isPerson(value)) {
  // value は Person
}
TypeScript

「if の条件が、そのまま日本語の文章として読めるか?」
を基準に名前をつけると、
コード全体の読み心地が一気に良くなります。


まとめ:ユーザー定義型ガードを自分の言葉で言うと

最後に、あなた自身の言葉でこう整理してみてください。

ユーザー定義型ガードは、

  • 戻り値型に value is 型 と書くことで、
    「true のときに引数の型を絞り込む」特別な関数。
  • unknown やユニオン型から、
    「この条件を満たすなら〇〇型だ」と TypeScript に教えるための仕組み。
  • 判定ロジックを関数に閉じ込めることで、
    if 文の中で安全にプロパティアクセスできるようにする。

設計するときは、

  • 何を判定したいかをはっきりさせる
  • 引数は広め(多くは unknown)に取る
  • true のときに本当にその型の条件を満たすように実装する
  • 同じチェックが何度も出てきたら、型ガードに切り出す

このあたりを意識してみてください。

そうすると、ユーザー定義型ガードは
「TypeScript の小技」ではなく、
“型とロジックをきれいに結びつけるための設計ツール”
として、あなたのコードを一段引き上げてくれます。

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