ユニオン型 (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()の戻り値を受けた定数numOrFalsenumber型と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);

このとき、引数の型をどちらかに確定させても戻り値の型に影響を与えません。他の言語にあるようなオーバーロードのような現象は起こりません。たとえばuni.makes()number型を引数として与えたとしても、これはuni.makes()number型を要求するA型と確定したと解釈されることはありません。

判別可能なユニオン型(Discriminated Unions)

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

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

ユニオン型のResponseはふたつのタイプエイリアスが持つsuccessを共通のプロパティとして持ちますが片方はtrueでもう片方はfalseです。ここはboolean型とせずあえてリテラル型にしています

そしてこの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 = {
present: false;
};
type Optional<T> = Some<T> | None;

リテラル型を使えば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;
}

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

リテラル型でなくても他の型どうしであればTypeScriptはこの判別を自動的にしてくれます。

type Measurement = {
b: number;
w: number;
h: number;
};
type TopSecret = {
b: 'secret';
w: 'secret';
h: 'secret';
};
type ThreeSize = Measurement | TopSecret;
const size: ThreeSize = measure();
if (size.b === 'secret') {
console.log(size.w);
// -> 'secret'
console.log(size.h);
// -> 'secret'
}

スリーサイズを公表したくない人は'secret'という文字をどこかひとつでもに入れておけばTopSecret型であると判別され、対応するifブロックではすべてのサイズは'secret'になります。

TypeScriptはこう解釈している

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