はじめに
昨今、AIの進歩は目覚ましく、私のような初心者エンジニアでもちゃちゃっと実装できてしまいます。
これは嬉しいことで、正しい使い方をすることでスキルアップの手助けにもなりますよね。
ただ懸念点もあります。その一つとしてAIが生成したコードをあまり理解せずにPRを出してしまうことが挙げられます。
any型は使うな
私は先日、このようなことがありました。
TypeScriptでの開発中にAIが生成したコードの中にany型が使用されていたのですが、そのままPRを出してしまいました。
先輩からは「any型は使わないように!」をレビューが帰ってきたのですが、正直なんでany型を使ってはいけないのか理解していませんでした。
私のような初心者エンジニアは「型エラーは消えるし動作もするし使ってもいいのでは?」と思ってしまいます。
今回はなぜany型を使用してはいけないのかを調べてみました。
anyを使ってはいけない理由
- 型安全性が失われる
- エディタの補完が効かなくなる
- リファクタリングに弱くなる
- レビュー・保守のコストが増加する
anyを使ってはいけない理由として上記のことが考えられます。
1つずつ詳しく見ていこうと思います。
型安全性が失われる
TypeScriptを使う最大の理由は 型によるエラー検知 です。
JavaScriptでは実行してみないとわからないエラーも、TypeScriptならコンパイル前に検知できます。これによって開発効率が上がり、バグの混入を防ぐことができます。
一方で anyは、TypeScriptの型システムを無視して「とりあえずエラーを消す」だけの存在です。
つまり、せっかくの型のメリットを自分で捨ててしまっているんですよね。
例えば次の関数を見てください。
const plusNumber = (num1: any, num2: any) => {
return num1 + num2;
}
plusNumber(10, 11) //21 <-意図した挙動
plusNumber("10", "11") //"1011" <-文字列結合になっちゃった。。。
anyを使っているせいで、数値以外でも何でも渡せてしまいます。
これを型で制約すれば、エディタ上でしっかりエラーを出してくれます。
const plusNumber = (num1: number, num2: number) => {
return num1 + num2;
}
plusNumber(10, 11) //21 <-意図した挙動
plusNumber("10", "11") //エディタ上でエラー表示
これなら「数値だけを受け取る関数」という意図が明確になります。
エディタの補完が効かなくなる
anyを使うと、エディタ(VSCodeなど)が「この変数の型が何かわからない」と判断します。
その結果、プロパティやメソッドの補完が出なくなります。
const user: any = { name: "Taro", age: 20 };
user. // ← 補完がほとんど出ない
これだと毎回ドキュメントを見に行ったり、間違ったキーを打っても気づけなかったりしてバグの原因になり得ます。
一方で型をちゃんと定義しておけば、エディタが賢く補完してくれます。
type UserType = { name: string; age: number };
const user: UserType = { name: "Taro", age: 20 };
user. // ← name と age が補完で出てくる!
補完機能は「正しい型情報」を前提に動いています。
anyを使ってしまうとその恩恵をまるごと捨てることになるんです。
リファクタリングに弱くなる
開発において「変数名を変える」「型を変更する」といったリファクタリングはよくあることです。
このとき、anyが使われているとエラーが出ないせいで不具合を見逃しやすいという問題があります。
例えば次のコードを見てください。
type UserType = {
name: string;
age: number;
};
const printUser = (user: any) => {
console.log(user.name);
};
const user: UserType = { name: "Taro", age: 20 };
printUser(user);
ここで「nameプロパティだとフルネームかどうか分かりにくいから、プロパティ名ををfullNameにリファクタリングしよう」となったとします。
type UserType = {
fullName: string;
age: number;
};
const printUser = (user: any) => {
console.log(user.name);
};
const user: UserType = { fullName: "Taro Yamada", age: 20 };
printUser(user); // ← ここ、エラー出ない!
一方でanyを使わずにちゃんと型を指定するとエディタが教えてくれます。
const printUser = (user: User) => {
console.log(user.name); // nameが存在しないとエラーが表示
};
これならリファクタリングの段階でエラーが検知できるので、バグを混入させるリスクを減らせることができます。
レビュー・保守のコストが増加する
any は「とりあえず動く」状態にしてしまうため、コードの意図が伝わらなくなる という問題もあります。
レビューする人や、未来の自分が読んだときに「ここって何が入る想定なんだっけ?」と悩むことになります。
例えばこんなコード。
const fetchData = (): any => {
return { id: 1, title: "記事タイトル" };
};
const data = fetchData();
console.log(data.name); // 実行時エラー
これだけだとレビュワーは「dataにどんな値が返ってくるのか」がコードから読み取れません。
しかも型が曖昧なせいで、リファクタや仕様変更のときにバグを生みやすくなります。
一方、型を定義してあればコードを見ただけで意図が伝わります。
type Article = { id: number; title: string };
const fetchData = (): Article => {
return { id: 1, title: "記事タイトル" };
};
const data = fetchData();
console.log(data.title); // 補完も効くし安全
型情報があることで「この関数は記事データを返す」と一目でわかります。
レビューの効率も上がり、保守のコストも下がります。
anyの回避手段
anyを使用してはいけない理由は分かりました。ただ、私も使いたくて使ってるわけではなく、分からないからanyしかなかったと言うのが正直なところです。
そこでanyを回避する方法をまとめてみました。
- unknown型を使用する
- ジェネリクスを使用する
- 型ナローイング、型ガード関数を使用する
unknown型を使用する
正直、anyとunknownって同じ様なものだと思っていたのですが、unknown型は以下のような特性があるそうです。(TypeScriptの公式ドキュメントに書いていたものを和訳しました。)
TypeScript 3.0で導入された「unknown」型は、「any」型の安全な代替です。
TypeScript 3.0では、新しいトップ型「unknown」が導入されました。「unknown」は「any」の型安全な代替です。
「unknown」には、次の特徴があります。
• どんな値でも「unknown」型に代入できます。
• しかし、「unknown」型の値は、型アサーションや型の絞り込み(narrowing)をしない限り、「unknown」自身と「any」以外の型には代入できません。
• また、「unknown」型の値には、型を絞り込むかアサーションしない限り、プロパティアクセスや関数呼び出しなどの操作はできません。
つまりこれはunknownは“わからない”を表すけど、使う前にチェックが必須ということです。
そのためanyとは違って安全に開発が可能になります。
以下は簡単な具体例です。
function handle(value: unknown) {
if (typeof value === "string") {
console.log(value.toUpperCase()); // OK: ここで string に絞れた
} else {
// 文字列以外の時の処理
}
}
ジェネリクスを使用する
一言でいうと、ジェネリクスは「型の箱 を用意して、呼び出し側の型をそのまま保つ仕組み」です。そのためanyのように型を捨てず安全です。
TypeScript公式にも以下のように書いてあります。
any型を使うことで、確かに関数はどんな型でも受け取れるようになりますが、実際には関数が返す値の型に関する情報を失ってしまいます。
ジェネリクスを使うことで、引数として渡された型の情報を保持したまま返すことができます。
例えば、number型を渡した場合、返り値もnumber型になります。
これにより、any型を使った場合のように型情報が失われることなく、型安全性を保ったまま関数を汎用的に使うことができます。
以下でanyを使用した例とジェネリクスを使用した例を比較したコード例を載せておきます。
// anyを使用した例
// 引数の型情報を捨てるので、戻り値も any になってしまう
function identityAny(value: any) {
return value;
}
const a = identityAny(123);
// a: any ← 数字を渡しても戻り値はany。number用メソッドの補完も弱い。
const b = a.toFixed(2); // これもコンパイルは通る(実行時まで保証されない)
// ジェネリクスを使用した例
// ジェネリクス版:渡された型 T をそのまま保って返す
function identity<T>(value: T): T {
return value;
}
const a = identity(123);
//a: number ← number を渡したのでnumberのまま
const b = a.toFixed(2); // OK(補完も効くし型安全)
型ナローイング、型ガード関数を使用する
ポイントは 「最初は“わからない”型でも、条件分岐で安全に“狭めて”から使う」こと。TypeScriptは typeof / in / instanceof などのチェックを型ガードとして理解し、分岐の内側だけ型を“より具体的”にしてくれます(これを narrowing と呼ぶらしい)。
以下はコード例
// typeof
function normalizeScore(s: number | string) {
return typeof s === "string" ? Number(s) : s;
// 渡ってくるのがnumberでもstringでもnumberに正規化できる
}
// in(プロパティの有無をチェックする)
type Baseball = { pitch(): void };
type Soccer = { shoot(): void };
function play(sport: Baseball | Soccer) {
if ("pitch" in sport) sport.pitch(); // Baseballに絞られる
else sport.shoot(); // Soccerに絞られる
}
// 配列かどうかをチェック
function head<T>(v: T | T[]) {
return Array.isArray(v) ? v[0] : v;
// 配列なら配列の先頭、単体ならそのまま返す(空配列は undefined)
}
// 判別可能ユニオン(タグで分岐)
type PlayBaseball =
| { kind: "pitch"; speed: number } // 球速 km/h
| { kind: "hit"; distance: number }; // 打球飛距離 m
function describe(p: PlayBaseball) {
switch (p.kind) {
case "pitch":
return `球速 ${p.speed}km/h`;
case "hit":
return `飛距離 ${p.distance}m`;
default: {
// これで新しいkindを追加し忘れたらコンパイル時に気付ける
const _exhaustive: never = p;
return _exhaustive;
}
}
}
// kindをタグにしてswitchするだけで、分岐ごとに型が自動でナローイングされる
describe({ kind: "pitch", speed: 160 });
describe({ kind: "hit", distance: 130 });
じゃあなんでany型は存在してるの?
一応TypeScriptの公式には以下の様に記述されていました。
一部の状況では、すべての型情報が利用できなかったり、型の宣言に不適切なほど多くの労力が必要になることがあります。
こうしたケースは、TypeScriptで書かれていないコードや、サードパーティのライブラリから値を受け取る場合などに発生します。
このような場合、型チェックを無効化したいことがあります。
そのために、値に any 型を付けます。any 型は、既存の JavaScript と連携する強力な方法であり、コンパイル時に型チェックを徐々に有効化・無効化できるようにします。
unknown 型とは異なり、any 型の変数は存在しないプロパティにも自由にアクセスできます。
これには関数も含まれ、TypeScript はその存在や型をチェックしません。any 型は、オブジェクト内でも伝播し続けます。
ただし、any の便利さは型安全性を失うことと引き換えです。
型安全性は TypeScript を使う主な動機のひとつなので、必要がない限り any 型の使用は避けるべきです。
一応使う場面はあるみたいですが、よほどのことが無い限りは使用は避けた方が良さそうですね。
まとめ
any型は便利ですが、それは一時的なものです。
バグの混入の原因にもなり得ますし、その場合は逆に面倒なことになる可能性が高いです。
anyを使う場面もあるみたいですが、極力避けるようにしようと感じました。
回避策についてもまだ理解が足りていない部分があるので、復習したいと思います。