TypeScriptへの取り組み
この書籍をサポートしてください: 購入 または 寄付
(広告、ブロックしないでください。)

14 型への特殊な値の追加



型を理解する方法の一つとして、値を集めた集合として捉えるという考え方があります。時には、値には2つのレベルが存在します。

この章では、ベースレベルの型に特殊な値を追加する方法について検討します。

14.1 インバンドでの特殊な値の追加

特殊な値を追加する方法の一つは、ベース型のスーパーセットである新しい型を作成し、その中で一部の値を特殊なものとして扱うことです。これらの特殊な値は番兵値と呼ばれます。これらの値は、通常の値の兄弟として、インバンド(同じチャネル内)に存在します。

例として、読み取り可能なストリームの次のインターフェースを考えてみましょう。

interface InputStream {
  getNextLine(): string;
}

今のところ、.getNextLine()はテキスト行のみを処理し、ファイルの終端(EOF)は処理しません。EOFのサポートをどのように追加できるでしょうか?

可能性としては、以下のようなものが考えられます。

次の2つのサブセクションでは、番兵値を導入する2つの方法について説明します。

14.1.1 型へのnullまたはundefinedの追加

厳格なTypeScriptを使用する場合、単純なオブジェクト型(インターフェース、オブジェクトパターン、クラスなどを介して定義されたもの)にはnullは含まれません。そのため、共用体型を使用して、ベース型stringに追加できる良い番兵値となります。

type StreamValue = null | string;

interface InputStream {
  getNextLine(): StreamValue;
}

これで、.getNextLine()によって返された値を使用するときは常に、TypeScriptは文字列とnullの両方の可能性を考慮することを強制します。たとえば、

function countComments(is: InputStream) {
  let commentCount = 0;
  while (true) {
    const line = is.getNextLine();
    // @ts-expect-error: Object is possibly 'null'.(2531)
    if (line.startsWith('#')) { // (A)
      commentCount++;
    }
    if (line === null) break;
  }
  return commentCount;
}

A行では、linenullである可能性があるため、文字列メソッド.startsWith()を使用できません。これは次のように修正できます。

function countComments(is: InputStream) {
  let commentCount = 0;
  while (true) {
    const line = is.getNextLine();
    if (line === null) break;
    if (line.startsWith('#')) { // (A)
      commentCount++;
    }
  }
  return commentCount;
}

これで、実行がA行に達したとき、linenullでないことが保証されます。

14.1.2 型へのシンボルの追加

番兵としてnull以外の値を使用することもできます。シンボルとオブジェクトは、それぞれが一意の識別子を持ち、他の値と誤解されることがないため、このタスクに最適です。

これは、シンボルを使用してEOFを表す方法です。

const EOF = Symbol('EOF');
type StreamValue = typeof EOF | string;

なぜtypeofが必要で、EOFを直接使用できないのでしょうか?それは、EOFが値であり、型ではないからです。型演算子typeofは、EOFを型に変換します。値と型の異なる言語レベルの詳細については、§7.7「2つの言語レベル:動的と静的」を参照してください。

14.2 アウトオブバンドでの特殊な値の追加

メソッドによって任意の値が返される可能性がある場合、どうすればよいでしょうか?ベース値とメタ値が混ざり合わないようにするにはどうすればよいでしょうか?これは、そのようなことが起こる可能性のある例です。

interface InputStream<T> {
  getNextValue(): T;
}

EOFにどの値を選択しても、誰かがInputStream<typeof EOF>を作成し、その値をストリームに追加するリスクがあります。

解決策は、通常の値と特殊な値を分離し、それらが混ざり合わないようにすることです。特殊な値が個別に存在することは、アウトオブバンド(異なるチャネル)と呼ばれます。

14.2.1 判別共用体

判別共用体は、すべて少なくとも1つの共通のプロパティ、いわゆる判別子を持つ複数のオブジェクト型に対する共用体型です。判別子は、オブジェクト型ごとに異なる値を持っている必要があります。これは、オブジェクト型のIDと考えることができます。

14.2.1.1 例:InputStreamValue

次の例では、InputStreamValue<T>は判別共用体であり、その判別子は.typeです。

interface NormalValue<T> {
  type: 'normal'; // string literal type
  data: T;
}
interface Eof {
  type: 'eof'; // string literal type
}
type InputStreamValue<T> = Eof | NormalValue<T>;

interface InputStream<T> {
  getNextValue(): InputStreamValue<T>;
}
function countValues<T>(is: InputStream<T>, data: T) {
  let valueCount = 0;
  while (true) {
    // %inferred-type: Eof | NormalValue<T>
    const value = is.getNextValue(); // (A)

    if (value.type === 'eof') break;

    // %inferred-type: NormalValue<T>
    value; // (B)

    if (value.data === data) { // (C)
      valueCount++;
    }
  }
  return valueCount;
}

最初は、valueの型はInputStreamValue<T>(A行)です。次に、判別子.typeの値'eof'を除外し、その型はNormalValue<T>(B行)に絞り込まれます。これが、C行でプロパティ.dataにアクセスできる理由です。

14.2.1.2 例:IteratorResult

イテレーターを実装する方法を決定する際、TC39は固定された番兵値を使用したくありませんでした。そうしないと、その値がイテラブルに現れてコードが壊れる可能性があるためです。1つの解決策は、反復を開始するときに番兵値を選択することでした。代わりに、TC39は共通プロパティ.doneを持つ判別共用体を選択しました。

interface IteratorYieldResult<TYield> {
  done?: false; // boolean literal type
  value: TYield;
}

interface IteratorReturnResult<TReturn> {
  done: true; // boolean literal type
  value: TReturn;
}

type IteratorResult<T, TReturn = any> =
  | IteratorYieldResult<T>
  | IteratorReturnResult<TReturn>;

14.2.2 その他の種類の共用体型

他の種類の共用体型も、共用体のメンバ型を区別する手段がある限り、判別共用体と同じくらい便利です。

1つの可能性は、固有のプロパティを介してメンバ型を区別することです。

interface A {
  one: number;
  two: number;
}
interface B {
  three: number;
  four: number;
}
type Union = A | B;

function func(x: Union) {
  // @ts-expect-error: Property 'two' does not exist on type 'Union'.
  // Property 'two' does not exist on type 'B'.(2339)
  console.log(x.two); // error
  
  if ('one' in x) { // discriminating check
    console.log(x.two); // OK
  }
}

別の可能性は、typeofおよび/またはインスタンスチェックを介してメンバ型を区別することです。

type Union = [string] | number;

function logHexValue(x: Union) {
  if (Array.isArray(x)) { // discriminating check
    console.log(x[0]); // OK
  } else {
    console.log(x.toString(16)); // OK
  }
}