Tackling TypeScript
この書籍をサポートしてください: 購入する または 寄付する
(広告です。ブロックしないでください。)

22 タイプガードとアサーション関数



TypeScriptでは、値の型が一部の操作に対して汎用すぎる場合があります。たとえば、ユニオン型です。この章では、次の質問に答えます。

22.1 静的型が汎用すぎるのはいつか?

静的型がどのように汎用すぎるかを確認するには、次の関数 getScore() を考えてみましょう。

assert.equal(
  getScore('*****'), 5);
assert.equal(
  getScore(3), 3);

getScore() のスケルトンは次のようになります。

function getScore(value: number|string): number {
  // ···
}

getScore() の本体内では、value の型が number なのか string なのかわかりません。知る前に、value を実際に操作することはできません。

22.1.1 if とタイプガードによる絞り込み

解決策は、実行時に typeof を使用して value の型を確認することです (行 A と行 B)。

function getScore(value: number|string): number {
  if (typeof value === 'number') { // (A)
    // %inferred-type: number
    value;
    return value;
  }
  if (typeof value === 'string') { // (B)
    // %inferred-type: string
    value;
    return value.length;
  }
  throw new Error('Unsupported value: ' + value);
}

この章では、型を値の集合として解釈します。(この解釈と別の解釈の詳細については、[コンテンツは含まれていません] を参照してください。)

行 A と行 B から始まる then ブロック内では、実行したチェックにより、value の静的型が変更されます。元の型 number|string のサブセットを操作しています。このように型のサイズを縮小することを *絞り込み* と呼びます。 typeof や同様の実行時操作の結果を確認することを *タイプガード* と呼びます。

絞り込みは value の元の型を変更するのではなく、チェックを通過するにつれてより具体的な型にするだけであることに注意してください。

22.1.2 switch とタイプガードによる絞り込み

if の代わりに switch を使用した場合でも、絞り込みは機能します。

function getScore(value: number|string): number {
  switch (typeof value) {
    case 'number':
      // %inferred-type: number
      value;
      return value;
    case 'string':
      // %inferred-type: string
      value;
      return value.length;
    default:
      throw new Error('Unsupported value: ' + value);
  }
}

22.1.3 型が汎用すぎる場合のその他のケース

型が汎用すぎる場合のその他の例を次に示します。

これらの型はすべてユニオン型であることに注意してください。

22.1.4 unknown

値が unknown の場合、ほとんど何もできず、最初に型を絞り込む必要があります (行 A)。

function parseStringLiteral(stringLiteral: string): string {
  const result: unknown = JSON.parse(stringLiteral);
  if (typeof result === 'string') { // (A)
    return result;
  }
  throw new Error('Not a string literal: ' + stringLiteral);
}

言い換えれば、unknown 型は汎用すぎるため、絞り込む必要があります。ある意味で、unknown もユニオン型です (すべての型のユニオン)。

22.2 組み込みタイプガードによる絞り込み

見てきたように、*タイプガード* は、オペランドが実行時に特定の条件を満たしているかどうかによって、true または false を返す操作です。TypeScript の型推論は、結果が true の場合にオペランドの静的型を絞り込むことで、タイプガードをサポートします。

22.2.1 厳密等価 (===)

厳密等価はタイプガードとして機能します。

function func(value: unknown) {
  if (value === 'abc') {
    // %inferred-type: "abc"
    value;
  }
}

一部のユニオン型では、=== を使用してコンポーネントを区別できます。

interface Book {
  title: null | string;
  isbn: string;
}

function getTitle(book: Book) {
  if (book.title === null) {
    // %inferred-type: null
    book.title;
    return '(Untitled)';
  } else {
    // %inferred-type: string
    book.title;
    return book.title;
  }
}

ユニオン型コンポーネントを含めるために === を使用し、除外するために !=== を使用することは、そのコンポーネントが *シングルトン型* (メンバーが 1 つだけの集合) である場合にのみ機能します。 null 型はシングルトン型です。唯一のメンバーは値 null です。

22.2.2 typeofinstanceofArray.isArray

これらは 3 つの一般的な組み込みタイプガードです。

function func(value: Function|Date|number[]) {
  if (typeof value === 'function') {
    // %inferred-type: Function
    value;
  }

  if (value instanceof Date) {
    // %inferred-type: Date
    value;
  }

  if (Array.isArray(value)) {
    // %inferred-type: number[]
    value;
  }
}

then ブロック内で value の静的型がどのように絞り込まれるかに注意してください。

22.2.3 演算子 in を使用した個別のプロパティのチェック

個別のプロパティをチェックするために使用される場合、演算子 in はタイプガードです。

type FirstOrSecond =
  | {first: string}
  | {second: string};

function func(firstOrSecond: FirstOrSecond) {
  if ('second' in firstOrSecond) {
    // %inferred-type: { second: string; }
    firstOrSecond;
  }
}

次のチェックは機能しないことに注意してください。

function func(firstOrSecond: FirstOrSecond) {
  // @ts-expect-error: Property 'second' does not exist on
  // type 'FirstOrSecond'. [...]
  if (firstOrSecond.second !== undefined) {
    // ···
  }
}

この場合の問題は、絞り込みを行わずに、型が FirstOrSecond である値のプロパティ .second にアクセスできないことです。

22.2.3.1 演算子 in は非ユニオン型を絞り込みません

残念ながら、in はユニオン型にのみ役立ちます。

function func(obj: object) {
  if ('name' in obj) {
    // %inferred-type: object
    obj;

    // @ts-expect-error: Property 'name' does not exist on type 'object'.
    obj.name;
  }
}

22.2.4 共有プロパティの値のチェック (判別可能なユニオン)

判別可能なユニオンでは、ユニオン型のコンポーネントには、コンポーネントごとに値が異なる 1 つ以上の共通プロパティがあります。このようなプロパティは *判別子* と呼ばれます。

判別子の値のチェックはタイプガードです。

type Teacher = { kind: 'Teacher', teacherId: string };
type Student = { kind: 'Student', studentId: string };
type Attendee = Teacher | Student;

function getId(attendee: Attendee) {
  switch (attendee.kind) {
    case 'Teacher':
      // %inferred-type: { kind: "Teacher"; teacherId: string; }
      attendee;
      return attendee.teacherId;
    case 'Student':
      // %inferred-type: { kind: "Student"; studentId: string; }
      attendee;
      return attendee.studentId;
    default:
      throw new Error();
  }
}

前の例では、.kind は判別子です。ユニオン型 Attendee の各コンポーネントには、このプロパティがあり、一意の値が設定されています。

if 文と等価チェックは、switch 文と同様に機能します。

function getId(attendee: Attendee) {
  if (attendee.kind === 'Teacher') {
    // %inferred-type: { kind: "Teacher"; teacherId: string; }
    attendee;
    return attendee.teacherId;
  } else if (attendee.kind === 'Student') {
    // %inferred-type: { kind: "Student"; studentId: string; }
    attendee;
    return attendee.studentId;
  } else {
    throw new Error();
  }
}

22.2.5 ドット表記名の絞り込み

プロパティの型 (プロパティ名のチェーンを介してアクセスするネストされたプロパティの型も含む) を絞り込むこともできます。

type MyType = {
  prop?: number | string,
};
function func(arg: MyType) {
  if (typeof arg.prop === 'string') {
    // %inferred-type: string
    arg.prop; // (A)

    [].forEach((x) => {
      // %inferred-type: string | number | undefined
      arg.prop; // (B)
    });

    // %inferred-type: string
    arg.prop;

    arg = {};

    // %inferred-type: string | number | undefined
    arg.prop; // (C)
  }
}

前のコードのいくつかの場所を見てみましょう。

22.2.6 配列要素型の絞り込み

22.2.6.1 配列メソッド .every() は絞り込みません

.every() を使用してすべての配列要素が null 以外であることを確認した場合、TypeScript は mixedValues の型を絞り込みません (行 A)。

const mixedValues: ReadonlyArray<undefined|null|number> =
  [1, undefined, 2, null];

if (mixedValues.every(isNotNullish)) {
  // %inferred-type: readonly (number | null | undefined)[]
  mixedValues; // (A)
}

mixedValues は読み取り専用である必要があることに注意してください。そうでない場合、それへの別の参照によって、if 文内で nullmixedValues にプッシュすることが静的に許可されます。しかし、それは mixedValues の絞り込まれた型を正しくないものにします。

前のコードでは、次の *ユーザー定義タイプガード* を使用しています (それが何であるかについては、後ほど詳しく説明します)。

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

NonNullable<Union> (行 A) は、ユーティリティ型 であり、ユニオン型 Union から undefined 型と null 型を削除します。

22.2.6.2 配列メソッド .filter() は、より狭い型の配列を生成します

.filter() は、より狭い型の配列を生成します (つまり、既存の型を実際に絞り込むわけではありません)。

// %inferred-type: (number | null | undefined)[]
const mixedValues = [1, undefined, 2, null];

// %inferred-type: number[]
const numbers = mixedValues.filter(isNotNullish);

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

残念ながら、タイプガード関数を直接使用する必要があります。タイプガードを持つアロー関数は十分ではありません。

// %inferred-type: (number | null | undefined)[]
const stillMixed1 = mixedValues.filter(
  x => x !== undefined && x !== null);

// %inferred-type: (number | null | undefined)[]
const stillMixed2 = mixedValues.filter(
  x => typeof x === 'number');

22.3 ユーザー定義タイプガード

TypeScript では、独自のタイプガードを定義できます。たとえば、次のようになります。

function isFunction(value: unknown): value is Function {
  return typeof value === 'function';
}

戻り値の型 value is Function は *型述語* です。これは isFunction() の型シグネチャの一部です。

// %inferred-type: (value: unknown) => value is Function
isFunction;

ユーザー定義タイプガードは常にブール値を返す必要があります。 isFunction(x)true を返すと、TypeScript は実引数 x の型を Function に絞り込みます。

function func(arg: unknown) {
  if (isFunction(arg)) {
    // %inferred-type: Function
    arg; // type is narrowed
  }
}

TypeScript は、ユーザー定義タイプガードの結果をどのように計算するかは気にしないことに注意してください。これにより、使用するチェックに関して大きな自由度が得られます。たとえば、isFunction() は次のように実装することもできます。

function isFunction(value: any): value is Function {
  try {
    value(); // (A)
    return true;
  } catch {
    return false;
  }
}

残念ながら、unknown 型では行 A の関数呼び出しを実行できないため、パラメータ valueany 型を使用する必要があります。

22.3.1 ユーザー定義タイプガードの例: isArrayWithInstancesOf()

/**
 * This type guard for Arrays works similarly to `Array.isArray()`,
 * but also checks if all Array elements are instances of `T`.
 * As a consequence, the type of `arr` is narrowed to `Array<T>`
 * if this function returns `true`.
 * 
 * Warning: This type guard can make code unsafe – for example:
 * We could use another reference to `arr` to add an element whose
 * type is not `T`. Then `arr` doesn’t have the type `Array<T>`
 * anymore.
 */
function isArrayWithInstancesOf<T>(
  arr: any, Class: new (...args: any[])=>T)
  : arr is Array<T>
{
  if (!Array.isArray(arr)) {
    return false;
  }
  if (!arr.every(elem => elem instanceof Class)) {
    return false;
  }

  // %inferred-type: any[]
  arr; // (A)

  return true;
}

行 A では、arr の推論された型が Array<T> *ではない* ことがわかりますが、チェックにより、現在はそうなっています。そのため、true を返すことができます。TypeScript は私たちを信頼し、isArrayWithInstancesOf() を使用すると Array<T> に絞り込みます。

const value: unknown = {};
if (isArrayWithInstancesOf(value, RegExp)) {
  // %inferred-type: RegExp[]
  value;
}

22.3.2 ユーザー定義タイプガードの例: isTypeof()

22.3.2.1 最初の試み

これは、TypeScript で typeof を実装するための最初の試みです。

/**
 * An implementation of the `typeof` operator.
 */
function isTypeof<T>(value: unknown, prim: T): value is T {
  if (prim === null) {
    return value === null;
  }
  return value !== null && (typeof prim) === (typeof value);
}

理想的には、文字列 (つまり、typeof の結果の 1 つ) を介して value の予期される型を指定できるはずです。しかし、その場合、その文字列から型 T を派生させる必要があり、その方法がすぐにはわかりません (後ほど説明するように、方法があります)。回避策として、T のメンバー prim を介して T を指定します。

const value: unknown = {};
if (isTypeof(value, 123)) {
  // %inferred-type: number
  value;
}
22.3.2.2 オーバーロードの使用

より良い解決策は、オーバーロードを使用することです (いくつかのケースは省略されています)。

/**
 * A partial implementation of the `typeof` operator.
 */
function isTypeof(value: any, typeString: 'boolean'): value is boolean;
function isTypeof(value: any, typeString: 'number'): value is number;
function isTypeof(value: any, typeString: 'string'): value is string;
function isTypeof(value: any, typeString: string): boolean {
  return typeof value === typeString;
}

const value: unknown = {};
if (isTypeof(value, 'boolean')) {
  // %inferred-type: boolean
  value;
}

(このアプローチは、Nick Fisher によるアイデアです。)

22.3.2.3 インターフェースを型マップとして使用

別の方法として、インターフェースを文字列から型へのマップとして使用する方法があります (いくつかのケースは省略されています)。

interface TypeMap {
  boolean: boolean;
  number: number;
  string: string;
}

/**
 * A partial implementation of the `typeof` operator.
 */
function isTypeof<T extends keyof TypeMap>(value: any, typeString: T)
: value is TypeMap[T] {
  return typeof value === typeString;
}

const value: unknown = {};
if (isTypeof(value, 'string')) {
  // %inferred-type: string
  value;
}

(このアプローチは、Ran Lottem によるアイデアです。)

22.4 アサーション関数

アサーション関数は、パラメータが特定の基準を満たしているかどうかを確認し、満たしていない場合は例外をスローします。たとえば、多くの言語でサポートされているアサーション関数の1つは、assert()です。 assert(cond)は、ブール条件condfalseの場合に例外をスローします。

Node.jsでは、assert()組み込みモジュールassertを介してサポートされています。次のコードは、A行でそれを使用しています

import assert from 'assert';
function removeFilenameExtension(filename: string) {
  const dotIndex = filename.lastIndexOf('.');
  assert(dotIndex >= 0); // (A)
  return filename.slice(0, dotIndex);
}

22.4.1 TypeScriptのアサーション関数サポート

TypeScriptの型推論は、アサーション関数を_アサーションシグネチャ_を戻り値の型としてマークすると、特別なサポートを提供します。関数の戻り値とその方法に関して、アサーションシグネチャはvoidと同等です。ただし、さらに絞り込みをトリガーします。

アサーションシグネチャには2種類あります

22.4.2 ブール値の引数をアサートする: asserts «cond»

次の例では、アサーションシグネチャasserts conditionは、パラメータconditiontrueでなければならないことを示しています。そうでない場合は、例外がスローされます。

function assertTrue(condition: boolean): asserts condition {
  if (!condition) {
    throw new Error();
  }
}

assertTrue()は、このように絞り込みを行います

function func(value: unknown) {
  assertTrue(value instanceof Set);

  // %inferred-type: Set<any>
  value;
}

引数value instanceof Setを型ガードと同様に使用していますが、条件文の一部をスキップする代わりに、falseが例外をトリガーします。

22.4.3 引数の型をアサートする: asserts «arg» is «type»

次の例では、アサーションシグネチャasserts value is numberは、パラメータvalueが型numberでなければならないことを示しています。そうでない場合は、例外がスローされます。

function assertIsNumber(value: any): asserts value is number {
  if (typeof value !== 'number') {
    throw new TypeError();
  }
}

今回は、アサーション関数を呼び出すと、引数の型が絞り込まれます

function func(value: unknown) {
  assertIsNumber(value);

  // %inferred-type: number
  value;
}
22.4.3.1 アサーション関数の例: オブジェクトにプロパティを追加する

関数addXY()は、既存のオブジェクトにプロパティを追加し、それに応じて型を更新します

function addXY<T>(obj: T, x: number, y: number)
: asserts obj is (T & { x: number, y: number }) {
  // Adding properties via = would be more complicated...
  Object.assign(obj, {x, y});
}

const obj = { color: 'green' };
addXY(obj, 9, 4);

// %inferred-type: { color: string; } & { x: number; y: number; }
obj;

交差型S & Tは、型Sと型Tの両方のプロパティを持ちます。

22.5 クイックリファレンス: ユーザー定義型ガードとアサーション関数

22.5.1 ユーザー定義型ガード

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

22.5.2 アサーション関数

22.5.2.1 アサーションシグネチャ: asserts «cond»
function assertTrue(condition: boolean): asserts condition {
  if (!condition) {
    throw new Error(); // assertion error
  }
}
22.5.2.2 アサーションシグネチャ: asserts «arg» is «type»
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new Error(); // assertion error
  }
}

22.6 アサーション関数の代替

22.6.1 テクニック: 強制変換

アサーション関数は、既存の値の型を絞り込みます。強制変換関数は、新しい型を持つ既存の値を返します。次に例を示します

function forceNumber(value: unknown): number {
  if (typeof value !== 'number') {
    throw new TypeError();
  }
  return value;
}

const value1a: unknown = 123;
// %inferred-type: number
const value1b = forceNumber(value1a);

const value2: unknown = 'abc';
assert.throws(() => forceNumber(value2));

対応するアサーション関数は次のようになります

function assertIsNumber(value: unknown): asserts value is number {
  if (typeof value !== 'number') {
    throw new TypeError();
  }
}

const value1: unknown = 123;
assertIsNumber(value1);
// %inferred-type: number
value1;

const value2: unknown = 'abc';
assert.throws(() => assertIsNumber(value2));

強制変換は、アサーション関数の用途以外にも使用できる汎用的な手法です。たとえば、次のように変換できます

詳細については、[コンテンツは含まれていません]を参照してください。

22.6.2 テクニック: 例外をスローする

次のコードについて考えてみましょう

function getLengthOfValue(strMap: Map<string, string>, key: string)
: number {
  if (strMap.has(key)) {
    const value = strMap.get(key);

    // %inferred-type: string | undefined
    value; // before type check

    // We know that value can’t be `undefined`
    if (value === undefined) { // (A)
      throw new Error();
    }

    // %inferred-type: string
    value; // after type check

    return value.length;
  }
  return -1;
}

A行で始まるif文の代わりに、アサーション関数を使用することもできます

assertNotUndefined(value);

そのような関数を記述したくない場合は、例外をスローするのが簡単な代替手段です。アサーション関数を呼び出すのと同様に、この手法も静的型を更新します。

22.7 @hqoss/guards: 型ガード付きライブラリ

ライブラリ@hqoss/guardsは、TypeScript用の型ガードのコレクションを提供します。次に例を示します