ユニオン型 (Union types)

大元が動的な型付言語であるJavaScriptのため、成功した時はnumber型を返すけれど、失敗した時はfalseを返すといった関数を提供するパッケージもあります。つまりA型のときもあればB型の時もある。ということをひっくるめたい時にこのユニオン型が使えます。

ユニオン型の宣言

上記例のユニオン型を受ける変数は以下のように宣言できます。

const numOrFalse: number | false = union();

関数の戻り値についても同様に書くことができます。

function union(): number | false {
// ....
return false;
}

タイプエイリアスに使うこともできます。

type PHPLikeReturnValue = number | false;

型を|で並べるだけです。上記例は2つの例ですが、いくつでも並べることができます。

配列やジェネリクスで注意すること

以下のような宣言は、おそらく希望通りの結果になりません。

string | number[];
Array<number> | string;

これは両方ともstring型またはnumber[]型であることを意味します。

正しくは以下です。特に配列をarray方式で書いている時は()が必要になるので注意してください。

(string | number)[];
Array<string | number>;

TypeScriptはどう解釈するか

上記の関数union()の戻り値を受けた定数numOrFalseのあとに.をつけるとnumber型とboolean型のどちらもが持っているメソッド、プロパティが入力補完候補に表示されます。

以下のようなクラスBeastBirdがあったとします。

class Beast {
public legs: number = 4;
public walk(): void {
// ...
}
public alive(): boolean {
return true;
}
}
class Bird {
public legs: number = 2;
public wings: number = 2;
public fly(): void {
// ...
}
public alive(): boolean {
return true;
}
}
function union(): Beast | Bird {
// ...
}
const creature: Beast | Bird = union();
creature.

するとこのユニオン型を返す関数を受けた定数creatureBeastBirdが共に持つプロパティ、メソッドが使用できます。つまりこの場合creature.legscreature.alive()が入力補完候補として表示されます。

ユニオン型で注意すること

ユニオン型になった元の型がもっているプロパティ、メソッドが同じでも戻り値の型が異なる場合は、その戻り値もまたユニオン型になります。 以下のクラスA, Bを考えます。

class A {
public does(): number {
return 1;
}
public makes(num: number): number {
return num;
}
}
class B {
public does(): string {
return '';
}
public makes(str: string): string {
return str;
}
}
function union(): A | B {
// ...
}
const uni: A | B = union();

uni.does()は以下のようなメソッドとして解釈されます。

const done: string | number = uni.does();

また同じメソッド名でありながら引数の型が違うメソッドがあれば、その引数もユニオン型になります。さらに戻り値の型も異なれば同じようにユニオン型になります。

つまりuni.makes()は以下のようなメソッドであると解釈されます。

const made: string | number = uni.makes(arg: string | number);

この時、引数の型を確定させても、戻り値の型に影響を与えません。他の言語にあるようなオーバーロード(Overloads)のような現象は起こりません。例えばuni.makes(1)number型を引数に入れたとしても、これはuni.makes(arg)number型を要求するA型と確定したと解釈されることはありません。よって戻り値madestring | number型のままです。

ちなみにTypeScriptにも関数のオーバーロードが存在します。

ユニオン型から型を確定させる

JavaScriptはその変数がどの型かを確定させる機能があります。もちろんTypeScriptにも存在し、その機能を用いて型が確定できた場合、TypeScriptはその型として見なしてくれます。この時よく使うのはtypeofinstanceofです。主にプリミティブ型に対してはtypeofを、クラスに対してはinstanceofを使えば問題ありません。

const prim: number | string = unionPrimitive();
if (typeof prim === 'number') {
prim.
return;
}
prim.
const creature: Beast | Bird = unionClass();
if (creature instanceof Bird) {
creature.
}

上記例ではtypeofnumber型と確定したifのブロックの中、つまり4行目で.をつけるとnumber型のプロパティ、メソッドが入力補完候補として現れます。 同様にinstanceofBirdと確定したifブロックの中、つまり13行目で.をつけるとBirdのインスタンスが持つプロパティ、メソッドが表示されます。

またユニオン型が上記のprimcreatureのように2択で、typeofinstanceofifのブロック内でreturnが行われれば、ifのブロックより下ではもう片方の型であると自動的に型を確定してくれます。上記例8行目のprimがそれにあたります。入力補完候補もstring型のものが表示されるようになります。

定数を持たせる方法で型を確定させる(Discriminated unions)

以下のようなタイプエイリアスのSuccessResponseErrorResponseを考え、そのユニオン型としてResponseを考えます。

type SuccessResponse = {
success: true;
response: Data;
};
type ErrorResponse = {
success: false;
error: Error;
};
type Response = SuccessResponse | ErrorResponse;

ユニオン型のResponseは2つのタイプエイリアスが持つsuccessを共通のプロパティとして持ちますが片方はtrueでもう片方はfalseです。

そしてこのRequestを返す関数req()があり、それを呼び戻り値を定数resで受けたとすると以下のようなことができます。

const res: Response = req();
if (res.success) {
// res.response ...
} else {
// res.error ...
}

ifの条件がtrueになる、つまりres.successtrueになるとそのブロックではres.responseが入力補完候補として表示されるようになります。一方elseのブロックではres.errorが入力補完候補として表示されるようになります。これはres.successtrueの場合はSuccessResponseであることが確定しfalseの場合はErrorResponseであることが確定するからです。

値があるかもしれないしないかもしれないことを意味するOptionalをユニオン型を使って表現するとこのようになるでしょう。

type Some<T> = {
present: true;
value: T;
};
type None<T> = {
present: false;
};
type Optional<T> = Some<T> | None<T>;

定数であればboolean型の変数true, falseに限らず他の型でも可能です。

type English = {
iso639: 'en';
thanks: 'thank you very much';
};
type French = {
iso639: 'fr';
merci: 'merci beaucoup';
};
type German = {
iso639: 'de';
danke: 'danke schön';
};
type Langauge = English | French | German;
const lang: Langauge = select();
switch(lang.iso639) {
case 'en':
return lang.thanks;
case 'fr':
return lang.merci;
case 'de':
return lang.danke;
}

上記例ではstring型のlang.iso639がそれに該当します。

switchを使いましたが、switchの時はfallthroughが発生する可能性があるためbreakreturnがないと2つ目より下では入力補完候補に制限がでてしまうことに注意してください。

TypeScriptはこう解釈している

ユニオン型に所属するひとつのデータ型は、ユニオン型の派生型(subtype)として解釈されます。この使い方はジェネリクスで頻出します。