ユニオン型は、string | numberのような記法で「stringまたはnumber」のような意味の型を作る方法です。TypeScriptプログラミングではユニオン型は非常に便利で、様々なインターフェースを的確に型で表現するためには欠かせない道具です。
ユニオン型を得るためには上の例のように|記法を使いますが、この記事では|と書かずに型推論を用いてユニオン型を得る方法を集めてみました。
構文系
JavaScriptの構文の意味から型推論でユニオン型が推論される系です。
条件分岐
条件分岐の構文では、ランタイムの条件によって結果が変わるため、コンパイル時には結果がどちらに分岐するか分かりません。そのため、TypeScriptコンパイラはどちらでも対処できるようにユニオン型を推論します。
条件演算子
let c = Math.random() ? "pikachu" : 25;
type T = typeof c;
こうするとTはstring | number型となります。|を使わずにユニオン型を得ることができました。
if文
if文などを使う場合でもユニオン型を得ることができます。この場合は関数の返り値の型の推論と組み合わせましょう。
function f() {
if (Math.random()) {
return "pikachu" + "";
} else {
return 25 + 0;
}
}
// type T = string | number
type T = ReturnType<typeof f>;
あるいは、letの型の推論と組み合わせることもできます。
let v;
if (Math.random()) {
v = "pikachu";
} else {
v = 25;
}
// type T = string | number
type T = typeof v;
型ガードと||
TypeScriptではtypeof v === "string"のような条件式(型ガード)によってvの型をstringに絞り込んだりすることができます。複数の型ガードを||で繋げるとユニオン型になります。
function f(val: unknown) {
if (typeof val === "string" || typeof val === "number") {
// type T = string | number
type T = typeof val;
}
}
|を使わないんだから||も使いたくないという場合は、&&を使うこともできます。
function f(val: unknown) {
if (!(typeof val !== "string" && typeof val !== "number")) {
// type T = string | number
type T = typeof val;
}
}
またtypeof val === "object"でunknownを絞り込むとobject | nullというユニオン型を作ることができます。
function f(val: unknown) {
if (typeof val === "object") {
// type T = object | null
type T = typeof val;
}
}
配列リテラル
TypeScriptでは、as constを付けない配列リテラルはT[]の形の型に推論されます。配列の中身に複数の型の値がある場合、Tはユニオン型となります。
const arr = ["pikachu", 25];
// type T = string | number
type T = typeof arr extends (infer T)[] ? T : never;
型計算系
純粋な型の計算でユニオン型を作る系です。
タプル型を使う
タプル型に対してnumberでlookupするとタプル型の各要素のユニオン型を得ることができます。
// type T = string | number
type T = [string, number][number];
@kazatsuyu さんのご指摘で追加しました。ありがとうございました。
オプショナルなプロパティを使う
オブジェクトのオプショナルなプロパティの型を取得すると、自動的にundefinedとのユニオン型になります。
// type T = string | undefined
type T = {a?: string}['a']; // string | undefined
@cisdur7 さんのご指摘で追加しました。ありがとうございました。
ちなみに、TypeScript 4.1で追加されたTemplate Literal Typesを用いると、次のようにしてstring | numberなど任意のユニオン型を作ることができます。
type PU = {a?: "pikachu"}['a'];
// type T = string | number
type T = { pikachu: string; undefined: number }[`${PU}`];
keyofを使う
次のように複数のキー名を持つオブジェクト型に対してkeyofを用いるとユニオン型が得られます。これは、Objを{ pikachu: number } & { pichu: number }と見て、オブジェクトのキー名が反変の位置にあることを用いてインターセクション型をユニオン型に変換しているという見方もできます。
type Obj = {
pikachu: number;
pichu: number;
};
// type T = "pikachu" | "pichu"
type T = keyof Obj;
inferを使う
複数の位置で同じ型変数をinferすると、ユニオン型が推論されます(反変の位置にある場合はインターセクション型になります)。
type Obj = {
pikachu: string;
pichu: number;
}
// type T = string | number
type T = Obj extends { pikachu: infer T; pichu: infer T } ? T : never;
Conditional Typesを使う
Conditional typesは条件分岐を表す型ですが、特定の条件で両辺のユニオン型に解決される場合があります。
type A<V> = V extends boolean ? string : number;
// type T = string | number
type T = A<any>;
Mapped Typesを使う
TypeScript 4.1で追加されたMapped Typesのkey remappingを使うと、複数のキーに由来する型を同じキーに集めることでユニオン型を発生させられます。
type Obj = {
pikachu: string;
pichu: number;
};
type Obj2 = {[K in keyof Obj as "raichu"]: Obj[K]};
// type T = string | number
type T = Obj2["raichu"];
タプル型を使う (2)
TypeScript 4.0で追加されたVariadic Tuple Typesでは、[string, ...T, string]のような型を書くことができるようになりました。しかし、もし例えばTがnumberだった場合に、結果は[string, ...number[], string]とはなりません。可変長部分よりも後ろに固定長の部分が来ることはできないからです。
ただし、TypeScript 4.2ではこの点が改良されて[string, ...number[], string]もOKになります。ただし可変長部分はタプル型の中に1つだけであり、[string, ...number[], string, ...number[], string]のようなものは依然として不可能です。
このようなTypeScriptで許可されていないタプル型を作ろうとした際にはTypeScriptで扱える形に強制的に変換され、その際にユニオン型が生じることがあります。
type A<N extends any[]> = [string, ...N, string, ...N, string];
// type T = string | number
type T = A<number[]>[1];
まとめ
この記事では、|を使わずにユニオン型を得る方法をまとめました。これでキーボードが壊れて|が入力できなくなっても安心ですね。
思いつく限り記載しましたが、もしかしたら抜けがあるかもしれません。見つけた方はぜひ教えてください。