🍺

any型を排除し、型安全なコードに変える【TypeScript】

2024/12/26に公開

こんにちは。今回は最近取り組んだTypeScriptのリファクタリングについてシェアしたいと思います。
具体的には、プロジェクトで多用されていたany型をなくす作業を行いました。振り返りも兼ねて、なぜany型が問題なのか、どうやって対処したのかをまとめてみました。

リファクタリングの背景

現在関わっているプロジェクトは約4年前に始まり、私はちょうど1年前に(ほとんどコーディングをする)システムエンジニアとして配属されました。しかし、このプロジェクトには少しクセがありました。仕様が頻繁に変わったり、メンバーが次々と入れ替わったりと、なかなか安定しない環境でした。その結果、急いで動くためにany型を使ったり、不必要なコードが増えたりしていました。TypeScriptを使っているにもかかわらず、型安全を無視したコードが目立つのが現状でした。

そこで、今回any型を排除し、TypeScriptの本来の力を活かすためのリファクタリングを提案しました。上長にも了承をもらい、実際に作業に取り掛かりました。

本題

any型って、一見すると便利そうですよね。どんな型の値でも受け入れてくれるから、急いで動かしたいときには助かります。
しかし、開発する際には、実行される前にエラーを検出できる方が望ましいです。

喫茶店で例えると、any型を使うことは、オーダーがあってドリンクを作り始めた後にオーダーミスだとわかるようなもので、型を使うことは、作り始める前にオーダーミスを教えてくれるようなものです。

any型を使う場面

主にこんなときにany型を使いがちです

  • 型が分からないとき: どんなデータが来るかわからないから、とりあえずanyにして動かす。
  • 型エラーが出て面倒なとき: エラーが出るけど、急いで対応したいからanyにして一時的に回避。

any型の問題点

でも、any型にはこんなデメリットがあります

  • 型安全性がなくなる: TypeScriptの型チェックが効かなくなるので、実行時エラーが増えるリスクが高まります。
  • コードが読みにくくなる: 型が分からないと、他の開発者(自分も含めて)がコードを理解しづらくなります。
  • リファクタリングが大変: 型情報がないと、コードを変更するときに予期せぬバグが出やすくなります。
  • IDEの機能が活かせない: 型情報がないと、オートコンプリートやリファクタリング支援が使えなくなります。

実際に、先日機能改修をしたときにも、any型が原因でコードがブラックボックス化していて、どんな型が扱われているのか分からなくなってしまいました。この先コードが増えるとバグも増えやすくなるので、早めに対策を考えないといけないと感じていました。

実際にやってみたリファクタリングのステップ

具体的にどんな風にany型をなくしたのか、手順を紹介します。

  1. any型を探す: プロジェクト内でany型が使われている箇所を特定します。コードを辿っていき、どこからデータ型が途切れているのかを確認し、データの型を把握します。
  2. 適切な型を定義する: 各any型に対して、適切な型を定義します。
  3. 型ガードを導入する: 型が不明なデータに型ガードを入れて、安全に型を絞り込みます。
  4. テストを充実させる: リファクタリング後のコードが正しく動くか確認するために、ユニットテストや統合テストを増やします。
  5. コードレビューを行う: チームメンバーにコードレビューをお願いして、型の定義やリファクタリングの妥当性を確認してもらいます。

今回は、上記の23について詳しく書いていきたいと思います。

例の紹介では、現実世界にイメージを近づけて考えたいので
喫茶店でオーダーされた飲み物のうち、コーヒーの焙煎度のデータが欲しい。
という要望がお店側からあったとしてサンプルコードを書いてみます。
コーヒーが好きなので(照)

1. 型を明確に定義する

できるだけ、変数や関数の型を明示的に定義します。具体的な型を定義することで、TypeScriptの型チェックがしっかり働きます。

悪い例

let Order1: any = {
    size: "Medium",
    isHot: true,
    price: 500,
    roast: "浅煎り"
};

良い例

const Order1: Coffee = {
    size: "Medium",
    isHot: true,
    price: 500,
    roast: "深煎り"
};

2. 型ガードを使う

型が不明なデータを扱うときは、型ガードを使って安全に型を絞り込みます。

悪い例

function orderDrink(drink: any) {
    if (drink.roast) {
        console.log(`Coffee Roast: ${drink.roast}`);
    } else {
        console.log(`Tea Type: ${drink.type}`);
    }
}

// サンプルデータ
const Order1 = {
    size: "ミディアム",
    isHot: true,
    price: "四百五十", // priceが数値ではなく文字列
    roast: "中煎り"
};

const Order2 = {
    size: "レギュラー",
    isHot: true,
    price: 500,
    type: "アールグレイ",
    flavor: "フルーティー" // 不要なプロパティ
};

const Order3 = {
    size: "Large",
    isHot: false,
    price: 500,
    // roastとtypeの両方が存在しない
};

// 関数の呼び出し
orderDrink(Order1); // 出力: コーヒーの焙煎度: 中煎り(一応出力はされる)
orderDrink(Order2); // 出力: お茶の種類: アールグレイ(一応出力はされる)
orderDrink(Order3); // 出力: Tea Type: undefined

良い例

// コーヒーの型定義
type Coffee = {
    size: string;    // サイズ("Small", "Medium", "Large")
    isHot: boolean;  // hotかどうか
    price: number;   // 価格
    roast: string;   // 焙煎度(""light", "medium", "dark")
}

// teaの型定義
type Tea = {
    size: string;
    isHot: boolean;
    price: number;
    type: string;    // teaの種類(例: "アールグレイ", "緑茶")
}

// 型ガード関数の定義
function isCoffee(drink: Coffee | Tea): drink is Coffee {
    return 'roast' in drink;
}

function orderDrink(drink: Coffee | Tea) {
    if (isCoffee(drink)) {
        console.log(`コーヒーの焙煎度: ${drink.roast}`);
    } else {
        console.log(`お茶の種類: ${drink.type}`);
    }
}

// サンプルデータ
const Order1: Coffee = {
    size: "ミディアム",
    isHot: true,
    price: 450,
    roast: "中煎り"
};

const Order2: Tea = {
    size: "レギュラー",
    isHot: true,
    price: 400,
    type: "アールグレイ"
};

const Order3: any = {
    size: "Large",
    isHot: false,
    price: 500,
    // roastもtypeも存在しない
};

orderDrink(Order1); // 出力: コーヒーの焙煎度: 中煎り
orderDrink(Order2); // 出力: お茶の種類: アールグレイ
orderDrink(Order3); // TypeScriptの型チェックによりエラーが発生

解説

  • 型ガード関数は、TypeScriptにおいて特定の条件に基づいて変数の型を絞り込むための関数です。
    つまりisCoffeeは、渡されたdrinkCoffee型であるかどうかを判定するための型ガード関数です。
    これにより、条件分岐後のコードブロック内で変数の型が明確になり、型安全にプロパティやメソッドを使用できるようになります。

  • 引数: drink: Coffee | Tea
    この関数は、drinkという引数を受け取ります。このdrinkCoffee型または Tea型のいずれかです。

  • 戻り値の型: drink is Coffee
    これは型述語と呼ばれるもので、この関数が true を返す場合、TypeScriptに対して drinkCoffee型であることを伝えます。

  • 関数の内容: 'roast' in drink
    drink オブジェクトに roast プロパティが存在するかどうかを確認します。これにより、drinkCoffee 型であることを確認できます。

※ただ型ガード関数は危険性も含んでいるので、使用には注意が必要です。今回は触れませんが、また近いうちに投稿できればと思います。

まとめ

any型は一見便利ですが、実際には型安全性を損なう大きな原因になります。特に長期間続くプロジェクトや大規模なシステム開発では、any型を多用するとバグが増えやすくなり、メンテナンスも大変になります。

今回のリファクタリングを通じて、any型をなくすことでコードの品質と保守性が向上したと思っています。TypeScript本来の力を活かして、堅牢なシステムを作るためにも、any型はできるだけ避けるべきだと実感しました。

これからもTypeScriptのベストプラクティスを取り入れて、継続的にリファクタリングを行い、プロジェクトの品質を高めていきたいと思います。

Discussion