KotlinでUnion Typeを使いこなそう
はじめに
こんにちは、株式会社ログラスでエンジニアをしています、Kyosukeです。
私は前職では主にTypescriptでフロントエンド・バックエンドのどちらも開発を行っていたのですが、ログラスではフロントエンドにTypescript、バックエンドにKotlinを使用しています。
そしてKotlinを書いていると、定期的にTypeScriptで言う、Union Typeを使いたくなることがあります。
そこで、この記事では、TypeScriptでお馴染みのUnion TypeをKotlinでどう扱うかを探っていきます。
TypeScriptでのUnion Type
まずはTypeScriptにおけるUnionTypeについて復習しましょう。
TypeScriptでは、異なる型を一つの変数で扱いたくなる場合にUnion Typeを使います。
これは複数の型のうちの一つを受け入れることができるという意味で、|
演算子を用いて型を結合することができます。
サンプルコード
以下の例では、Success
とError
という2つの異なる型をResult
というUnion Typeで表現しています。
interface Success {
type: 'success';
message: string;
}
interface MyError {
type: 'error';
error: string;
}
type Result = Success | MyError;
function handleResult(result: Result) {
switch (result.type) {
case 'success':
console.log(`Success: ${result.message}`);
break
case 'error':
console.error(`Error: ${result.error}`);
break;
}
}
// 使用例
handleResult({ type: 'success', message: '操作が成功しました。' });
handleResult({ type: 'error', error: '何らかのエラーが発生しました。' });
このTypeScriptのコードでは、Success
とMyError
という2つのインターフェースを定義し、それぞれtype
プロパティを持たせています。
これにより、関数handleResult
内での型チェックをtype
プロパティに基づいて行っています。
このようにしてTypeScriptではUnion Typeを使って、異なる型を柔軟に扱うことができます。
KotlinでのUnion Type
Kotlinを書いていくと、TypeScriptのように気軽にUnion Typeを使えないことに気づくかもしれません(私はそうでした)。
しかし、KotlinにはUnion Typeに相当する機能があり、それを使うことでTypeScriptのような柔軟な型の取り扱いを実現することができます。
ただし、TypeScriptのように直接的なUnion Typeをサポートしていないため、多少の工夫が必要になります。
サンプルコード
Kotlinでは、Union Typeに相当する機能を実現するためにいくつかの方法がありますが、最も一般的な方法はsealed class
を利用することです。
sealed class
は、限定されたサブクラスを持つことができるクラスであり、これを使うことで、TypeScriptのUnion Typeのようなパターンを模倣することが可能です。
sealed class Result
data class Success(val message: String) : Result()
data class Error(val error: String) : Result()
fun handleResult(result: Result) {
when (result) {
is Success -> println("Success: ${result.message}")
is Error -> println("Error: ${result.error}")
}
}
// 使用例
handleResult(Success("操作が成功しました。"))
handleResult(Error("何らかのエラーが発生しました。"))
このコード例では、Result
というsealed class
を定義し、そのサブクラスとしてSuccess
とError
という2つのデータクラスを定義しています。
関数handleResult
では、when
式を使って、結果がSuccess
型かError
型かに応じた処理を行っています。
これにより、TypeScriptの例と同様に、異なる型に基づいたロジックの分岐を安全に実装することができます。
少し複雑なUnion Typeの取り扱い
先ほどの例はかなりシンプルな例でしたが、もう少し複雑なUnion Typeの取り扱いを考えてみましょう。
TypeScriptの例
ここでは円と四角形の面積を計算する関数を例に考えます。
この場合、Circle
とSquare
という2つの型を持つUnion Typeを使って、円と四角形の面積を計算する関数を実装します。
type Shape = Circle | Square;
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
}
}
Kotlinでの対応
それでは、このTypeScriptの例をKotlinでどのように実装するか見ていきましょう。
sealed class Shape
data class Circle(val radius: Double) : Shape()
data class Square(val sideLength: Double) : Shape()
fun getArea(shape: Shape): Double =
when (shape) {
is Circle -> Math.PI * shape.radius * shape.radius
is Square -> shape.sideLength * shape.sideLength
}
実際のコードでは、より複雑なUnion Typeを扱うことになると思いますが、基本的にはこのように、sealed class
とwhen
式を組み合わせることで、TypeScriptのUnion Typeに相当する機能をKotlinで実現することができます。
when
式は、TypeScriptのswitch
文とどう違うのか
KotlinのKotlinでsealed class
を利用する際、when
式が非常に重要な役割を果たします。
特に、Union Type的なパターンを扱うケースでは、when
式の網羅性がコードの安全性と信頼性を大きく向上させます。
網羅性の担保とTypeScriptの比較
TypeScriptの例:
type PaymentStatus = "pending" | "completed" | "failed";
const handlePaymentStatus = (status: PaymentStatus) => {
switch (status) {
case "pending":
console.log("支払いが処理中です。");
break;
case "completed":
console.log("支払いが完了しました。");
break;
case "failed":
console.log("支払いに失敗しました。");
break;
default:
const exhaustiveCheck: never = status;
break;
}
};
このTypeScriptの例では、PaymentStatus
型の網羅性をチェックしています。
default
ケースでnever
型を使用しているため、新たなPaymentStatus
の値を追加した場合にコンパイルエラーが発生し、網羅性が担保されます。
Kotlinでの対応:
Kotlinではsealed class
を使い、when
式で同様の網羅性をシンプルに担保できます。
sealed class PaymentStatus
object Pending : PaymentStatus()
object Completed : PaymentStatus()
object Failed : PaymentStatus()
fun handlePaymentStatus(status: PaymentStatus) {
when (status) {
is Pending -> println("支払いが処理中です。")
is Completed -> println("支払いが完了しました。")
is Failed -> println("支払いに失敗しました。")
// Kotlinの`when`式は全てのケースを網羅している必要があり、
// ここでは`else`ケースは不要です。
}
}
when
式の利点
-
型安全性の強化:
when
式を使うことで、Kotlinは全てのケースが網羅されているかをコンパイル時にチェックします。これにより、未処理のケースがあればコンパイルエラーとなり、意図しない挙動やランタイムエラーを事前に防ぎます。 -
コードの明瞭さ:
else
ケースを記述する必要がないため、処理すべき具体的なケースが一目で明確になります。これは、コードの読みやすさと保守性を向上させる効果があります。
Kotlinのwhen
式は、Union Type的なパターンを扱う際の網羅性をシンプルかつ効率的に担保する強力なツールです。
この特性を活用することで、Kotlinでの型安全性を高めつつ、読みやすく保守しやすいコードを実現できます。
おまけ: より高度なパターンマッチを求めて
実はここが一番のお気に入りセクションですので、ぜひ読んでいってください。
Kotlinのwhen式や、TypeScriptのswitch文は、パターンマッチングを行う際に非常に便利な機能ですが、時にはもう少し高度なパターンマッチングを行いたいという場面もあるかもしれません。
その場合、KotlinやTypeScriptの標準機能だけでは限界があるかもしれません。
そんな皆様におすすめしたいのが、Rustという言語です。Rustはいいぞ。
公式記事より抜粋していくつかご紹介しますので、気になる方はぜひRustを触ってみてください。
複数の変数を一度にマッチングする
これはよくあるケースではないでしょうか。
Kotlinでこれを実現するには、Pair
やTuple
を使って、いったん1つの変数にまとめる必要がありますが、Rustでは以下のように非常にシンプルに記述することができます。
fn main() {
let hoge = 2;
let fuga = 2;
match (hoge, fuga) {
(2, 2) => println!("wow!"),
_ => println!("anything"),
}
}
範囲条件を使う
次は、範囲条件を使ったパターンマッチングです。
Kotlinではこのような条件を使う場合、when
式の中で..
を使って範囲を指定する必要がありますが、Rustでは以下のように非常にシンプルに記述することができます。
fn main() {
let x = 5;
match x {
// 1から5まで
1..=5 => println!("one through five"),
// それ以外
_ => println!("something else"),
}
}
マッチ条件に追加で条件をつける
最後に最強の機能を紹介します。
正直、これがあれば何でもできるのでは?と思うほどの機能ですが、用法容量は守って使っていきましょう。
これもKotlinではwhen
式の中でif
を使って条件を追加する必要がありますが、Rustでは以下のように非常にシンプルに記述することができます。
fn main() {
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(n) if n == y => println!("Matched, n = {:?}", n),
_ => println!("Default case, x = {:?}", x),
}
println!("at the end: x = {:?}, y = {:?}", x, y);
}
この場合では、x
がSome
であり、かつその値がy
と等しい場合にMatched, y = 10
と出力されます。
まとめ
この記事では、TypeScriptでお馴染みのUnion TypeをKotlinでどのように扱うかを探求しました。
KotlinにはTypeScriptのようなUnion Typeが直接存在しないものの、sealed class
とwhen
式を駆使することで、同等の柔軟性と型安全性を達成する方法を見てきました。
-
sealed class
の利用: Kotlinでは、sealed class
を通じて限定されたサブクラスを持つことができ、これを用いることでTypeScriptのUnion Typeと同様のパターンを表現できます。 -
when
式の網羅性:when
式を使ってsealed class
のインスタンスを扱うことで、全てのケースがコンパイル時に網羅されていることを保証します。これは、TypeScriptのswitch
文とnever
型を用いた網羅性のチェックに類似していますが、Kotlinではさらに直接的にこの保証を得ることができます。 - 型安全性の向上: これらの手法を用いることで、Kotlinの強力な型システムの恩恵を受けつつ、TypeScriptでの経験を活かすことが可能になります。また、コードの読みやすさと保守性の向上にも寄与します。
KotlinとTypeScriptは異なる言語でありながら、型の扱い方において互いから学べる点が多々あります。
TypeScriptのUnion Typeの柔軟性をKotlinで実現する方法を理解することで、より安全で保守しやすいアプリケーションの開発が可能になるでしょう。
最後までお読みいただき、ありがとうございました!
Discussion