null
またはundefined
の追加型を理解する方法の一つとして、値を集めた集合として捉えるという考え方があります。時には、値には2つのレベルが存在します。
この章では、ベースレベルの型に特殊な値を追加する方法について検討します。
特殊な値を追加する方法の一つは、ベース型のスーパーセットである新しい型を作成し、その中で一部の値を特殊なものとして扱うことです。これらの特殊な値は番兵値と呼ばれます。これらの値は、通常の値の兄弟として、インバンド(同じチャネル内)に存在します。
例として、読み取り可能なストリームの次のインターフェースを考えてみましょう。
interface InputStream {getNextLine(): string;
}
今のところ、.getNextLine()
はテキスト行のみを処理し、ファイルの終端(EOF)は処理しません。EOFのサポートをどのように追加できるでしょうか?
可能性としては、以下のようなものが考えられます。
.getNextLine()
を呼び出す前に呼び出す必要がある追加のメソッド.isEof()
。.getNextLine()
がEOFに達したときに例外をスローする。次の2つのサブセクションでは、番兵値を導入する2つの方法について説明します。
null
またはundefined
の追加厳格なTypeScriptを使用する場合、単純なオブジェクト型(インターフェース、オブジェクトパターン、クラスなどを介して定義されたもの)にはnull
は含まれません。そのため、共用体型を使用して、ベース型string
に追加できる良い番兵値となります。
type StreamValue = null | string;
interface InputStream {getNextLine(): StreamValue;
}
これで、.getNextLine()
によって返された値を使用するときは常に、TypeScriptは文字列とnull
の両方の可能性を考慮することを強制します。たとえば、
function countComments(is: InputStream) {
= 0;
let commentCount while (true) {
= is.getNextLine();
const line // @ts-expect-error: Object is possibly 'null'.(2531)
if (line.startsWith('#')) { // (A)
++;
commentCount
}if (line === null) break;
};
return commentCount }
A行では、line
がnull
である可能性があるため、文字列メソッド.startsWith()
を使用できません。これは次のように修正できます。
function countComments(is: InputStream) {
= 0;
let commentCount while (true) {
= is.getNextLine();
const line if (line === null) break;
if (line.startsWith('#')) { // (A)
++;
commentCount
}
};
return commentCount }
これで、実行がA行に達したとき、line
がnull
でないことが保証されます。
番兵としてnull
以外の値を使用することもできます。シンボルとオブジェクトは、それぞれが一意の識別子を持ち、他の値と誤解されることがないため、このタスクに最適です。
これは、シンボルを使用してEOFを表す方法です。
= Symbol('EOF');
const EOF type StreamValue = typeof EOF | string;
なぜtypeof
が必要で、EOF
を直接使用できないのでしょうか?それは、EOF
が値であり、型ではないからです。型演算子typeof
は、EOF
を型に変換します。値と型の異なる言語レベルの詳細については、§7.7「2つの言語レベル:動的と静的」を参照してください。
メソッドによって任意の値が返される可能性がある場合、どうすればよいでしょうか?ベース値とメタ値が混ざり合わないようにするにはどうすればよいでしょうか?これは、そのようなことが起こる可能性のある例です。
<T> {
interface InputStreamgetNextValue(): T;
}
EOF
にどの値を選択しても、誰かがInputStream<typeof EOF>
を作成し、その値をストリームに追加するリスクがあります。
解決策は、通常の値と特殊な値を分離し、それらが混ざり合わないようにすることです。特殊な値が個別に存在することは、アウトオブバンド(異なるチャネル)と呼ばれます。
判別共用体は、すべて少なくとも1つの共通のプロパティ、いわゆる判別子を持つ複数のオブジェクト型に対する共用体型です。判別子は、オブジェクト型ごとに異なる値を持っている必要があります。これは、オブジェクト型のIDと考えることができます。
InputStreamValue
次の例では、InputStreamValue<T>
は判別共用体であり、その判別子は.type
です。
<T> {
interface NormalValue: 'normal'; // string literal type
type: T;
data
}
interface Eof {: 'eof'; // string literal type
type
}type InputStreamValue<T> = Eof | NormalValue<T>;
<T> {
interface InputStreamgetNextValue(): InputStreamValue<T>;
}
function countValues<T>(is: InputStream<T>, data: T) {
= 0;
let valueCount while (true) {
// %inferred-type: Eof | NormalValue<T>
= is.getNextValue(); // (A)
const value
if (value.type === 'eof') break;
// %inferred-type: NormalValue<T>
; // (B)
value
if (value.data === data) { // (C)
++;
valueCount
}
};
return valueCount }
最初は、value
の型はInputStreamValue<T>
(A行)です。次に、判別子.type
の値'eof'
を除外し、その型はNormalValue<T>
(B行)に絞り込まれます。これが、C行でプロパティ.data
にアクセスできる理由です。
IteratorResult
イテレーターを実装する方法を決定する際、TC39は固定された番兵値を使用したくありませんでした。そうしないと、その値がイテラブルに現れてコードが壊れる可能性があるためです。1つの解決策は、反復を開始するときに番兵値を選択することでした。代わりに、TC39は共通プロパティ.done
を持つ判別共用体を選択しました。
<TYield> {
interface IteratorYieldResult?: false; // boolean literal type
done: TYield;
value
}
<TReturn> {
interface IteratorReturnResult: true; // boolean literal type
done: TReturn;
value
}
type IteratorResult<T, TReturn = any> =
| IteratorYieldResult<T>
| IteratorReturnResult<TReturn>;
他の種類の共用体型も、共用体のメンバ型を区別する手段がある限り、判別共用体と同じくらい便利です。
1つの可能性は、固有のプロパティを介してメンバ型を区別することです。
interface A {: number;
one: number;
two
}
interface B {: number;
three: number;
four
}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
} }