TypeScriptはJavaScriptに静的型を導入したプログラミング言語で、登場から現在までその人気を増し続けています。

動的型付き言語であるJavaScriptに静的型の安全性（コンパイル時にバグ・間違いを発見することができる能力）を与えることで、TypeScriptはJavaScriptによる開発の効率を上げてくれます。

裏にJavaScriptがあるという特性もあり、TypeScriptは「部分的に静的型チェックをする」というような挙動をサポートしています1。詳しくは後述しますが、これによりJavaScriptからTypeScriptへの移行が可能となっています。TypeScriptは @ts-check （あるいは @ts-ignore ）などを通じてこのようなユースケースも手厚くサポートしています。

このことの裏返しとして、TypeScriptを利用するときは注意すべき点があります。それは使い方によってはTypeScriptの能力が十分に発揮されず、安全性を享受できないという点です。記事タイトルにある敗北者のTypeScriptというのはもちろん筆者が造った言葉ですが、これはTypeScriptが持つ安全性を最大限享受できずに、それどころか逆に危険な状態でTypeScriptを使っていることを指します。

この記事ではどのようなときに敗北者のTypeScriptに陥ってしまうのか、そしてその対処法について議論します。

3行でまとめると

TypeScriptで --strict を使わない人や、 any とか as を濫用する人は敗北者です。

を使わない人や、 とか を濫用する人は敗北者です。 これらを使っていいのはあなたがTypeScriptよりも賢くて真にそれが必要だと分かっている場合だけです。

「敗北者のTypeScript」だからといってTypeScriptのメリットが完全に無くなるわけではありません。JSからの移行段階での敗北は仕方がないし、「素のJavaScript」よりも「敗北者のTypeScript」のほうが安全です。どんどんTypeScriptを使って敗北していきましょう。

敗北者のTypeScriptとは何か

この記事でいう敗北者のTypeScriptとは、ひとことで言えば any や as に代表される危険な機能を濫用するTypeScriptコードを指します。あとで詳説しますが、これらの要素はTypeScriptが本来保証してくれる安全性を破壊する機能であり、使えば使うだけTypeScriptを使うメリットが減少してしまいます。

これは筆者の意見ですが、最も理想的な状況としてはTypeScriptの strict オプションを有効にするのはもちろん（これは noImplicitAny や strictNullChecks などの厳しいチェックを有効にします）、 any や as などの危険な機能はeslint2のno-explicit-anyやconsistent-type-assertions3などのルールを用いて原則禁止としなければいけません。これらがどうしても必要な場合はいちいち // eslint-disable-next-line: no-explicit-any のようなコメントを用いてルールを明示的に無効化する必要があり、さらにコメントで any などが必要となる妥当な理由を説明していない場合はレビューで弾かれなければいけません。

TypeScriptを実際に使っている方の中には「そんな運用は現実的ではない」と思った方も多いでしょう。 any や as という抜け道が封じられるということは、開発にかかる時間やその他のコストが増加することも考えられます。また、上記の運用をするには何が any を使う“妥当”な理由であり何がそうでないのかを理解・判断する必要がありますが、そのためにはコードを書く人もレビューする人もTypeScriptに精通していなければいけません。そのような運用ができるチームを組むのは並大抵のことではないでしょう。

ゆえに、その通りです、TypeScriptユーザーの非常に多くはTypeScriptへの敗北に甘んじているのです。筆者ですら例外ではなく、業務では any を使用するコードに対して「 」とだけコメントしてLGTMを出したこともあります。

なお、ここまで読んだ方は既に察しているとは思いますが、筆者は型の信奉者であり過激派であるという点はご理解ください。実際、同じことをeslint-typescriptのリポジトリに書いたら「（ any を禁止するのはいいけど as を禁止するのは）俺だったらそんなん絶対嫌だわ（意訳）」と言われました。

ただ、思想はともあれ記事の内容は皆さんのTypeScriptコードをさらに安全にするのにきっと有用ですから、ぜひ読んでいってください。記事を読んで筆者と同じ思想に染まってくれたらさらに嬉しいです。

ところで、ここまで読んだ方の中にはなにが敗北だ、俺は売られた喧嘩は買う主義なんだぜと思っている方もいるかもしれません。この記事のタイトルは、筆者の別記事TypeScriptの型入門における any 型の説明に由来します。

ここで、any型という言葉が出てきましたので、これについても解説します。any型は何でもありな型であり、プログラマの敗北です。

つまり、負けたのは any を使ったプログラマです。そして、プログラマを打ち負かしたのはTypeScriptコンパイラです。敗北したプログラマはTypeScriptコンパイラと知恵比べを行いましたが、 any 無しではコンパイルを通すことができませんでした。そこで、プログラマは妥協し、安全性を犠牲にしてコンパイルを通すという選択をしたのです。

any は、プログラムの実態（＝プログラムが持つバグ・誤り）を何も変えないままに多くのコンパイルエラーを消す力を持ちます。つまり、 any を使うというのはTypeScriptがせっかく発見してくれた問題を放置し、TypeScriptを黙らせることを意味します。これこそがプログラマの敗北であり、プログラマは敗北の代償としてプログラムの安全性を失ったのです。

敗北の理由には、実力が足りない、あるいは時間が足りないといったことが挙げられるでしょう。 any を使わなくてもコンパイルが通るようなプログラムの設計や書き方ができないか、あるいはそのような良い書き方をする時間が無い場合に敗北が発生しがちです。

ここでは any が槍玉に挙がりましたが、 as の場合も同様です。これらはTypeScriptの安全性を脅かす言語機能第1位と第2位であり、どちらも使うとプログラムの安全性が犠牲になります。どう危険なのかということはこの記事でじっくり説明しますから、ご安心ください。

TypeScriptが敗北するとき

ところが、TypeScriptのスキルが一定以上ある方は、 any や as を全く使わずにTypeScriptプログラムを書くのは不可能であるということをご存知でしょう。TypeScriptコンパイラも完璧ではなく、プログラムのロジックを理解できなかった結果として、実際にはありえない型エラーを報告してくることがあります。 any や as はそのような場合にコンパイルエラーを消す手段として有効です。

これはプログラマの勝利、言い換えればTypeScriptの敗北です。書かれたプログラムがTypeScriptの推論能力を上回った結果として any や as の使用を余儀なくされることがあるのです。これが、冒頭で触れた「 any や as がどうしても必要な場合」です。

TypeScriptの敗北は型変数やconditional typeなどの複雑な機能を使う場合に多く発生しがちです。例えば、筆者の別記事TypeScriptの型レベル連結リスト活用術：型を変えられるコンテナを作るでは、 any や as がさりげなく使われているのを見ることができます。

any は as は使わないのが理想的ですが、実際のところそう上手くはいきません。プログラマの敗北かTypeScriptの敗北のいずれかの結果として、 any や as は必要となります。この記事の主張は、 any や as はTypeScriptの敗北の場合にのみ使うべきであるということです。それができなかった結果が、敗北者のTypeScriptです。

別の言い方をすれば、 any や as をプログラマの力不足を埋め合わせるために使ったのならばそれはプログラマの敗北であり、TypeScriptの力不足を埋め合わせるために使ったのならばそれはTypeScriptの敗北です。 any や as の使用が妥当かどうか見極めるには、どちらの力不足が原因なのかを見極める必要があります。ただし、これをきちんと見極めるのは並大抵のことではありません（それがなぜかは記事中で追々解説していきます。）。

前置きが長くなりましたが、いよいよ本題に入りましょう。まず、 any や as の何が危険なのか解説していきます。これを理解することは、 any や as を避けるべき理由を理解するだけでなく、プログラマの敗北とTypeScriptの敗北を見分けるためにも重要です。

any の危険性

any というのはTypeScriptが提供する特別な型です。 any 型はTypeScriptを黙らせるための型であり、 any 型に対して何をしてもTypeScriptは型エラーを出しません。

any 型が使われる場面は以下のように分類できます。

正しいコードを書いたのにTypeScriptの型推論能力が足りなくてエラーになるのでTypeScriptを黙らせたい場合（ TypeScriptの敗北 ）

） 実際コードが間違っていてTypeScriptがエラーを吐いているけど直せないのでとりあえずエラーを消したい場合（ プログラマの敗北 ）

） どんな型を書けばいいのか分からないのでとりあえず any と書いている場合（ プログラマの敗北 ）

と書いている場合（ ） JavaScriptからの移行の途中でまだ型を書いていないのでとりあえず any にしている場合（移行途中なら仕方ないけど敗北は敗北）

最初のひとつ以外は望ましい any の使用ではありません。また、後述しますが、最初のものも基本的には後述の as で代用可能です。

具体的な any 型の特徴は、任意の型の値を any 型として扱うことができること、そして any 型の値に対してどんな操作を行っても型エラーが起きない上、その結果は any 型になることです。では例を見ましょう。

// 関数fooは適当なオブジェクトを受け取ることができるつもり function foo ( obj : any ): number { // obj.numには数値が入っているはずなのでobj.numが存在すればそれを返すつもり if ( obj . num != null ) { return obj . num ; } // obj.numが存在しない場合もあるのでその場合は0を返す return 0 ; }

この例では、関数 foo の引数 obj の型を any としたことでこの関数の型安全性が崩壊しています。その結果、次のようなコードで問題が発生するでしょう。

foo ( null ); // これは型エラーは発生しないが実行時エラーが発生する // 変数valはTypeScript上ではnumber型なのに実際には"12345"という文字列が入る const val = foo ({ num : " 12345 " }); // 文字列にtoFixedメソッドは存在しないのでここで実行時エラーが発生する console . log ( val . toFixed ( 20 ));

このように、 any を使うと実際には危険なコードでも型エラーが起きません。まず、 foo の引数 obj は any 型なので、どんな値も受け入れます。その結果、型エラーを出すことなく foo に引数として { num: "12345" } を渡したり null を渡したりできます。

関数 foo の中ではまず obj.num というアクセスが発生しますが、 obj は any 型なのでもちろんエラーは発生しません。また、 obj は any 型なので obj.num も any 型として扱われます。

ここで any の危険性がひとつ露呈していますね。 obj に null とか undefined が入った場合は obj.num というプロパティアクセスは実行時エラーとなりますが、TypeScriptはそれを型エラーとして検出してくれませんでした。これは obj が any 型だからです。

さらに、 return obj.num; にも問題があります。関数 foo の宣言を見ると戻り値は number 型として宣言されていますので obj.num は number 型でなければなりませんが、 obj.num は any 型なのでそれを number 型として扱っても型エラーは起きません。明らかに、実際 obj.num に数値が入っている保証がありませんので、これは実行時のエラーに繋がります。

このように、 any 型を使うとTypeScriptは本来防げたはずの実行時エラーを防いでくれなくなります。これではTypeScriptを利用するメリットをプログラマ自ら殺していることになりますから、TypeScriptが提供する安全性を最大限享受するためには any を使うべきではありません。

特に、型をちゃんと書くのが面倒くさいから any にするというケースは最悪のパターンです。書こうと思えば書けた型を敢えて書かないというのは、（時間的要因など仕方ない部分があるかもしれませんが）明らかにプログラマの敗北です。

実際のところ、 any を使っているコードでも問題なく動作するという場合も多いでしょう。そもそも人類は型のないJavaScriptで沢山のプログラムを書いてきたのですから、 any の存在下で実際正しく動くプログラムを書くこともできるでしょう。そのようなコードを書いている方ならば、こんなのちょっと考えれば安全だって分かるんだから any を使ってもいいだろと思うかもしれません。しかし、筆者はそのような考え方は推奨しません。なぜなら、 any はコードの負債になるからです。

実際安全なコードであったとしても、 any とコードに書くということはそのコードが危険であると積極的に主張しているのも同然ですから、コードに書かれたその any はこれからそのコードを見るすべての人を警戒させ、思考時間を奪います。 any と書かなければTypeScriptが安全性を保証してくれたのに、 any と書いてしまったばかりにTypeScriptは型チェックを放棄し、そのコードの安全性を保証するのは人間の責任となるのです。これこそがまさに、TypeScriptを使う意味が薄れるということです。

この any がいかに安全かをコメントに書き残しておくという手もありますが、それなら any を消してちゃんと型を書いたほうがよいです。 any の安全性を証明するにはその any が関わるロジック全てを完全に精査し、コメントを読むだけで納得できるように分かりやすく説明しなければいけませんが、その労力はちゃんと型を書くために使うべきです。

もしどうしても正しい型が書けないとき、ほら見ろTypeScriptの敗北だと思われるかもしれませんが、それは少し早計です。プログラムの設計変更によってよりロジックを単純化することで、型が書けるようになる可能性があるからです。TypeScript時代においては、ちゃんと型で表現できるようにプログラムを設計するということもTypeScriptのスキルの一つですね。

そもそも型をどう書けばいいか分からないようなプログラムは、ロジックが複雑すぎます。TypeScriptがどうとか以前に、そのようなプログラムは書くべきではありません。人間がロジックを理解できないということは、たとえテストを書いても意味がなく、安全性を保証するのがたいへん困難だからです。

どうすればよかったのか

では、先ほどの any の例に話を戻して、これを正しく直してみましょう。いきなり正解を述べると、この場合は obj の型を any ではなく { num?: number } とすべきです。この型の意味は、「オブジェクトであり、プロパティ num を持っていても持っていなくてもいいが、持っている場合はプロパティ num は number 型でないといけない」という意味です4。この型を用いることで、関数 foo に変な値が渡されて実行時エラーとなるのを防止できます。

function foo ( obj : { num ?: number }): number { if ( obj . num != null ) { return obj . num ; } return 0 ; } // ↓これはコンパイルエラーになる foo ( null ); // ↓これもコンパイルエラーになる const val = foo ({ num : " 12345 " });

この例では、 obj の型に any を使うのをやめたことで実行時エラーに繋がるバグ（ foo(null) とか）を検出することができました。つまり、TypeScriptが提供する安全性を正しく活用できたことになります。

繰り返しますが、 obj に与えるべき型が分からずとりあえず any にしていたとしたら、それはプログラマの敗北としか言えませんね。正解が分からなかった方は、敗北者から抜け出すためにもっとTypeScriptの型を勉強するのが吉です。たとえば以下の記事などはその助けとなるでしょう（宣伝）。

もっと言えば、型が書けないときにTypeScriptの敗北であると主張するためには、TypeScriptでできることを全て知り尽くしていなければその資格がないということです。TypeScriptに勝利できるのはTypeScriptマスターだけなのです。

--noImplicitAny を使わない場合の危険性

もうひとつ any に関連する話題として、 --noImplicitAny の話をしましょう。これはTypeScriptのコンパイラオプションの一つで、 tsc のコマンドラインオプションか tsconfig.json を通じて指定します。

このオプションは暗黙に any 型を推論するのを避けるというオプションであり、もちろん利用すべきです。逆に、このオプションを使用しない場合は前述の危険な any 型が暗黙のうちに発生してしまうことがあり、非常に危険です。

まず、TypeScriptでは型宣言を省略することができる機能があります。これは、何も型が書いていないただのJavaScriptコードも受け付けられるようにしてJavaScriptからTypeScriptへの移行を支援するという目的があります。

// 関数fooの引数numの型と返り値の型が省略されている function foo ( num ) { return num . toFixed ( 3 ); } console . log ( foo ( 123 )); // "123.000"

では、上記のコードで num の型はどうなるでしょうか。我々の感覚だと、 foo(123) という呼び出しがあるので num の型は number になってほしいですね。ところが、実際には num の型は any となります。すなわち、関数引数の型が宣言されていない場合は問答無用で any になるのです5。この挙動は、JavaScriptからTypeScriptへ移行途中でまだ型が書けていない場合には便利です。まだ書かれていない変数の型をとりあえず any としておくことで、まだ型を書いていない部分で型エラーが発生するのを抑止してくれるのですから。

とはいえ、これはあくまで移行途中だから許される話であり、安全性を求めるならこのような挙動は言語道断です。そこで、このように any と書いていないのに any 型が発生する事態を防いでくれるのが --noImplicitAny オプションなのです。このオプションを有効にすると、上述のように引数の型が宣言されていない場合は問答無用でエラーになります。TypeScriptは関数のインターフェースは明示すべきという方針を採用しているように思われますから、関数の引数の型はちゃんと明示し、 --noImplicitAny オプションがオンの状態でもエラーが発生しないようにするのが正しいTypeScriptコードのあり方です6。

また、引数の型が書かれていないケース以外でもいくつかの場合（代表的なのは型定義の無いライブラリを読み込んだ場合）に any が発生することがあり、 --noImplicitAny はそのような挙動を全て防止してくれます。

--noImplicitAny は安全にTypeScriptを使うなら絶対に使うべきオプションですが、一つでも型の書いていない引数があればコンパイルエラーとなり、型定義が用意されていないライブラリを使うのもコンパイルエラーになるため、このオプションはJavaScriptからTypeScriptに移行している途中では使えないという特徴を持ちます。それゆえ、一部には「 --noImplicitAny を無理に使わなくてもいい」という主張をする人もいます。そのような考え方を否定はしませんが、それは any の自然発生という大敗北を受け入れるという、もはや不戦敗とも言えるやり方です。

筆者が危惧するのは、そのような負け犬根性が染み付いてしまい、それがTypeScriptの唯一のあり方であると思われることです。そのような主張が妥当性を持つのはあくまでJavaScriptからTypeScriptへの移行の途中の話であり、TypeScriptに移行が完了した後も --noImplicitAny オプションありでコンパイルが通らないようなコードは、敗北者のTypeScriptであることを免れません。

そろそろ顔が赤く染まってきた読者も多いと思うので再度述べておきますが、敗北者のTypeScriptを書くなと言っているわけではありません。それでもTypeScriptを使わないよりはずっと安全です。

ただ、ここでお伝えしたいのは、 --noImplicitAny を使わなくてもいいという主張は「 --noImplicitAny を使わないで達成できる程度の安全性で満足できるなら」という枕詞が付きます。 any が自然発生する状況では、例えば関数の引数の型を書き忘れるという単純なミスが、単なるコンパイルエラーでは済まずにランタイムエラー、壊れたロジック、その他重大なバグに繋がる可能性があります。 --noImplicitAny を使わないというのは、このように本来TypeScriptがコンパイルエラーという形で保証してくれる安全性を放棄するという明示的な意思表示に他なりません7。TypeScriptの安全性をどの程度享受し、そしてどの程度捨てるのかはプロジェクトが選択できることであり、TypeScriptの使われ方というのはその選択によって大きく異なったものとなります8。

ですから、 --noImplitiyAny は使わなくてもいいという主張を常に真に受けるのではなく、自分たちがどの程度の安全性を達成したいのかと天秤にかけて判断する必要があるのです。そして、自分たちの選択が自分たちにどの程度の安全性を齎し、また自分たちはどれくらい安全性を犠牲にしているのか、それを判断するのにこの記事は役に立つことでしょう。

この記事では以降も「〜すべき」などの言い回しを積極的に使いますが、それは「敗北者にならないためには」という前提の話です。ゆえに、あなたがどのレベルの安全性を求めるかによってはこの記事の主張は当てはまらないかもしれません。しかし、TypeScriptの真の力を知っていて敢えて使わないのか、それともそもそも真の力を知らないで使っているのかでは大違いです。では、次に進みましょう。

--strictNullChecks を使わない場合の危険性

TypeScriptの他の代表的なコンパイルオプションとしては --strictNullChecks が挙げられます。これは null 安全性（JavaScriptには null の他に undefined もあるのでそちらも）に関わるチェックを有効にするオプションです。

逆の言い方をすれば、このオプションを指定しないとTypeScriptの null 安全性が無に帰します。こんなのオプションにしないで常に null 安全にしろよとお思いだと思いますが、実は初期のTypeScriptには null 安全性が無く、後から追加で実装されたためこのような形になっています。それまでTypeScriptでコンパイルが通ってきた非 null 安全なコードのビルドを壊さないためにデフォルトでオンにするのは避けられたのです。例えば、 --strictNullChecks をオンにしないとこんなコードのコンパイルが通ります。

// fooは数値を受け取って文字列を返す関数 function foo ( num : number ) { return num . toFixed ( 3 ); } // fooにnullを渡すことができる！！！！！ foo ( null );

このように、どんな型に対しても null （とか undefined ）を渡すことができたのです。

--strictNullChecks オプションありならば、 null は number 型の値だと見なされなくなるのでこのコードはコンパイルエラーが発生します。嬉しいですね。

--strictNullChecks ありの状況では、 null を扱う場面では明示的に null 型を使用します。例えば、上記のコードを foo が null も受け取れるようにするには次のようにします。

// fooは数値またはnullを受け取って文字列を返す関数 function foo ( num : number | null ) { if ( num == null ) { // numがnullなら空文字列を返す return "" ; } // そうでなければnumはnumber型なので従来通り return num . toFixed ( 3 ); } // どちらもエラーが起きない foo ( null ); foo ( 123 );

引数 num の型を number | null としたことによって num が数値と null のどちらも受け取れるということを明示しました。また、コード中で if (num == null) という null チェックを行うことで null の処理を行います。TypeScriptはこのような処理を検知して、 num がいつ null でありいつ null でないかを推論してくれます。その結果として、if文の次の num.toFixed(3) の部分では num が null である可能性は既に排除されていると判断されて num は number 型となり、コンパイルエラーにならずに toFixed を呼び出すことができています。

この number | null は「 number 型または null 型」ということを表すunion型の記法です。TypeScript以外ではあまり見ないため戸惑う方もいるかもしれませんが、安全なTypeScriptプログラミングにおいては頻出の機能です。このように、TypeScriptの型の知識というのは安全にプログラムを書く能力に直結します。型の知識を付けるためには、例えば以下の記事がおすすめです（宣伝）。

その他のコンパイルオプションたち

TypeScriptの安全性は日進月歩で改善されており、時折新しい安全性チェックが導入されます。 --strictNullChecks と同様、それらは後方互換性を崩さないために原則としてコンパイルオプションの形で導入されます。その結果、これまで紹介した2つ以外にも安全性のチェックを強化するコンパイルオプションがいくつも提供しています。これらを有効にしないということはそれだけTypeScriptの安全性に穴を開けることになりますから、それらのコンパイルオプションは当然全て有効にすべきです。

そこで、これらのコンパイルオプション（前述の2つも含めて）をまとめて有効化できる素晴らしいオプションがあります。それが --strict です。ということは、 --strict オプションを有効化することが敗北者からの脱却の前提条件です。TypeScript側としても --strict オプションの利用を推奨しており、 tsc --init によって生成されるデフォルトの tsconfig.json ファイルは --strict オプションが有効化された状態となっています。

前述のような事情から、JavaScriptからTypeScriptへの移行案件の場合は --strict オプションをいきなり有効化するのは難しいでしょう。しかし、新規のTypeScript開発で --strict を有効にしないという選択はまず有り得ず、あったとすればそれは前述の負け犬根性の表れであると言わざるを得ません。敗北者のTypeScriptにすでに毒されています。コードを書かないことがバグを生まない最善の方法であることは広く知られていますが、 --strict を無効化するというのはまっさらの安全性100%の状態からコードを書き始める前に安全性を50%くらいまで落とすという行為に他ならないのです。

as の危険性

as はTypeScriptに特有の、型アサーションを行うための構文です。これは、型情報を強制的に修正するときに使います。まずは例を見てみましょう。

type MyObj1 = { id : number ; } type MyObj2 = { id : number ; name : string ; } function foo ( obj : MyObj1 ): string { // ↓これはエラー // return obj.name; // objはMyObj1型だけど無理やりMyObj2型に修正して利用 return ( obj as MyObj2 ). name ; }

関数 foo の中で as が使われていますね。関数 foo は obj.name を返したいのですが、 obj は MyObj1 型であり name プロパティが存在しないのでこれは型エラーとなります。そこで、エラーを回避するために一時的に obj の型を MyObj2 型にしてしまうのが (obj as MyObj2) です。これにより obj は MyObj2 型となり、 name プロパティを持っていると見なされるので型エラーが消えます。

これのどこが危険かはお分かりですね。 foo({ id: 123 }) のように実際は name プロパティを持たないオブジェクトを foo に渡すと返り値は undefined です。これは、 foo の返り値の型が string であることと合致していませんので、型情報と実行時の挙動が異なることになり実行時エラーに繋がります。

このように、 as はTypeScriptに文句を言わせずに型に対して危険な修正を行うことができる（具体的には型をその部分型に修正できる）機能です。ですから、正当な理由無く使うべきではありません。

まあ実際のところ、 any は絶対だめだけど as はまあ使ってもいいんじゃないという意見も見られます。それは、TypeScriptの敗北が発生したときに as を使って対応することが多いからです。というのも、TypeScriptの推論能力が足りないせいでコードに型エラーが発生する場合には、型が正しく推論されていない（実際のプログラムの動きと異なる・推論された型が実際に保証される条件よりも弱い）場合がほとんどです。この際に、 as を用いて正しい型に修正することで型エラーをなくすことができます。

逆に、型は正しいのに、その使い方が間違っているために型エラーが出るということもあります。上の例はこちらのパターンです。この場合、型を間違ったものに修正してコンパイルエラーを消すために as を使うことができます。これは言うまでもなくプログラマの敗北です。

つまり、 as を使いたい場合、それがどちらの敗北なのかを見極めるべきです。TypeScriptの敗北ならば as を使ってもよいですが、自分の敗北の場合に as を使うべきではありません。当然ながら、 as を使うためには、どちらの敗北か見極められるだけのTypeScript力が求められるということです。

ちなみに、 as は実行時には何も行いません。つまり、他の言語でいうキャストとは異なり、まずい型変換が行われたら実行時にエラーが出るわけではありません。ただ型チェック時に型が修正されるだけです。なので、型変換がまずいかもしれないので実行時にチェックしたいという場合は別途そのコードを書く必要があります。

as を使ってもよい例

基本的に、 as を使っていいのはあなたの書いたコードがTypeScriptの推論能力を凌駕したときです。ここではその一例を紹介します。

よくあるのは、オブジェクトをミュータブルなものとして扱う場面です。与えられたオブジェクトをコピーする関数を考えましょう。

function shallowCopy ( obj ) { const result = {}; for ( const key in obj ) { result [ key ] = obj [ key ]; } return result ; }

素朴にTypeScriptで型をつけようとするとこうなるでしょう。

function shallowCopy < T extends object > ( obj : T ): T { const result = {}; for ( const key in obj ) { result [ key ] = obj [ key ]; } return result ; }

しかし、これはエラーとなります。その理由は、 const result = {} によって result の型が {} と推論されるからです。これにより、 result[key] = obj[key] は result に対して未知のプロパティを勝手に足そうとしているのでエラーになります。また、 return result; は戻り値が T 型なのに {} 型の値を返そうとしているとしてエラーになります。

TypeScript側の主張も一理ありますね。実際、 const result = {}; の瞬間に result に入っているのは {} であり result はプロパティを何も持っていませんからこれの型は {} とせざるを得ません。

問題は、これを書いた人はこの関数内で result オブジェクトのプロパティがどんどん作られて最終的に T になることを意図しているのに、TypeScriptがそれを理解してくれないことです。実際のところ、コンパイラがこれを理解するのはかなり難しいでしょう。そこで、この場合は as を使ってエラーを消さなければなりません。

一番簡単なのは次のようにする方法です。

function shallowCopy < T extends object > ( obj : T ): T { const result = {} as T ; // {} を {} as T に変更 for ( const key in obj ) { result [ key ] = obj [ key ]; } return result ; }

as を使うことで、 {} の型を {} 型ではなく T 型だと思わせました。これにより変数 result の型も T 型になります。さっきの話からするとこれは嘘であり（変数 result が作られた瞬間は {} が T 型を満たすとは限らないため）、このコードは as によって型システムを欺く危険なコードとなっています。

この場合、 as による危険性が shallowCopy の中に閉じ込められているのがポイントです。 shallowCopy の内部でやっていることは危険ですが、 shallowCopy のインターフェース（ T 型の引数を受け取って T 型の値を返す）は正しいことは人間が注意深くチェックすれば確かめることができます。よって、 shallowCopy を使う側は内部の as による危険性を気にせずにこの関数を使うことができます。

まとめると、 as を正しく利用するためには以下のことに注意する必要があります。

TypeScriptの敗北を明らかにする 。この場合はTypeScriptが手続き的な操作でオブジェクトを作るコードを正しく型推論してくれないのが問題でした（やるのはかなり難しいでしょうから仕方ありませんが）。

。この場合はTypeScriptが手続き的な操作でオブジェクトを作るコードを正しく型推論してくれないのが問題でした（やるのはかなり難しいでしょうから仕方ありませんが）。 危険性の影響範囲をできるだけ小さい関数に閉じ込める。上の例で見たように、正しいインターフェースを持つ関数を定義し、 as の危険性をその内部でしか露呈しないようにしましょう。これにより、危険性が影響する範囲を明確にして人間による安全性のチェックの負担を減らすことができます。また、危険な as を使う目的が明確になります。

! の危険性

TypeScriptに独自の構文として後置の ! が知られています。これは値が null や undefined かもしれない可能性を無視するという構文です。とりあえず例を見てください。

type MyObj = { name ?: string ; } function foo ( obj : MyObj ): string { // obj.nameの最初5文字を返す（存在しない可能性は無視） return obj . name ! . slice ( 0 , 5 ); }

obj.name! というのは obj.name に後置の ! がついた構文です。これにより、 obj.name が存在しない可能性を無視しているのです。

というのも、 MyObj 型の宣言を見れば分かるとおり、 obj の name プロパティは存在しないかもしれません。これにより、 obj.name は string | undefined 型（ string 型かもしれないし undefined 型かもしれない値の型）となります。 undefined に対して slice メソッドを呼んだら実行時エラーになってしまいますから、それを防ぐために当然TypeScriptは型エラーを出します。

ここで、いやそんなの気にしないでいいからエラー出すなよと思った場合に obj.name! とすることで、これが undefined である可能性が排除されて string 型として扱われます。それにより、型エラーを発生させずに slice を呼ぶことができるのです。

ちなみに ! は as を使っても代用可能です。 as を使う場合は次のように書けます。これは obj.name の型を string | undefined から string に修正するということですね。

type MyObj = { name ?: string ; } function foo ( obj : MyObj ): string { return ( obj . name as string ). slice ( 0 , 5 ); }

ということで、 ! の危険性は基本的に as のそれと同じです。 ! をいつ使えばいいかとかそういう話も、基本的に as と同じことが当てはまります。

一応言っておくと、上記の例は明らかに ! の妥当な使い方ではありません。 foo に渡される obj が必ず name を持っていることが分かっているならば MyObj を修正すべきですし、 name を持っていないかもしれないなら foo の中の処理を修正すべきです。

! を使ってもよい例

基本的に ! に対する考え方は as と同じですが、わりと ! を使いそうな妥当な場面というのはあります。

type MyObj = { name ?: string ; } function foo ( obj1 : MyObj , obj2 : MyObj ): string { if ( obj1 . name == null && obj2 . name == null ) { // 両方ともnameを持っていない場合の処理 return "" ; } // （中略） if ( obj1 . name != null ) { // obj1がnameを持っていた場合の処理 return obj1 . name ; } else { // obj2がnameを持っていた場合の処理 return obj2 . name ; // ←ここで型エラーが発生！ } }

関数 foo は2つの MyObj 型のオブジェクトを受け取り、それらが両方とも name を持っていなかったときの処理を最初に行います。ということは、 foo の残りの部分では「 obj1.name と obj2.name の少なくとも一方は存在する」という条件が満たされることになりますが、TypeScriptはこのような「どちらか一方が成り立つけどどちらなのかは不明」というような条件を理解できません9。それにより、 if 文の else 部でエラーとなっています。人間ならば、この位置では obj1.name が存在しないので obj2.name が存在するであろうことが理解できますが、TypeScriptはこれが理解できません。よって、この場合は return obj2.name!; としてもまあ許されるでしょう。

なお、一応次のようにすることで ! の使用を回避することはできます。

if ( obj1 . name == null && obj2 . name == null ) { // 両方ともnameを持っていない場合の処理 return "" ; } // （中略） if ( obj1 . name != null ) { // obj1がnameを持っていた場合の処理 return obj1 . name ; } else if ( obj2 . name != null ) { // obj2がnameを持っていた場合の処理 return obj2 . name ; // ←エラーが起こらない } else { // ここは絶対に通らないけどTypeScriptのために仕方なく書いた部分 return "" ; }

これなら ! を使わずにエラーが回避できますが、代わりに絶対に使われない無駄なコードが発生してしまいました。カバレッジが無駄に下がるといった問題点もありますからどちらも一長一短でしょう。

その他の危険性

ここまでで紹介した any , as , ! くらいがTypeScriptが持つ代表的な危険性です。言うまでもなく、これらはTypeScriptの安全性の限界とかではなく、我々を甘やかし負け犬奴隷とするためにTypeScriptが用意した穴です。我々は厳しい自制心でもって、これらの利用を最低限に留める必要があります。

しかしながら、TypeScriptが持つ危険性というのはこれだけではありません。これまで見てきたような甘ったるいものではなく、単なる罠としか思えないものもあります。基本的にはこれらの危険性も、利便性と安全性のトレードオフにおいてTypeScriptの言語デザインとして利便性のほうを選択したことで起こっています。利便性のために存在している危険性であるということは、つい危険な要素を使ってしまいがちであるということです、それら全てに目を光らせるのは難易度が高いですが、できるだけこれを防ぐためにぜひ知っておくべきです。

これについては以下の記事によくまとまっています（筆者が書いた記事ではありません）。

is の活用

TypeScriptが提供する危険性のひとつが is です。 is 自体も正しく使わないとTypeScriptの安全性を破壊してしまう危険性を持ちますが、むしろ is は前述のように「危険性の影響範囲を小さくする」という目的で活用することができる諸刃の剣です。TypeScriptに勝利した機会に is を活用して危険性を最小に押し留めることができれば、勝利者の称号はあなたのものです。

では、まず is がどういう意味なのか見てみましょう。これは関数の返り値の型として使うことができる特殊な構文で、 引数名 is 型 という構文を持ちます。

function isStringArray ( obj : unknown ): obj is Array < string > { return Array . isArray ( obj ) && obj . every ( value => typeof value === " string " ); } function foo ( obj : unknown ) { if ( isStringArray ( obj )) { // この中ではobjはArray<string>型になる obj . push ( " abcde " ); } }

この例では isStringArray の返り値の型が obj is Array<string> です。このような型宣言を持つ関数は、返り値が真偽値でなければなりません。そしてこれは、返り値が true のとき obj の型は Array<string> とみなして良いという宣言になります。

よって、関数 foo の中の if 文で isStringArray(obj) が真かどうか判断したことによって、その中では obj を Array<string> 型の変数として使うことができるのです。

ちなみに、ここで登場している unknown という型は「どんな値でもよい」ことを表す型で、一見 any と似ていますが any のような危険性を持たず安全に使える型です。引数の型を unknown 型にするということは、どんな引数でも受け付ける（オブジェクトだろうと null だろうと何でも）ということを意味します。ですから、そのような引数を使いたい場合は if 文などで型の絞り込みを行わければいけません。そのような場合に is が役に立ちます。

is の危険性は、その関数が誤った判断を行えば間違った型がついてしまう点にあります。上記の関数を次のように変えると isStringArray 関数は嘘をついていることになりますが、TypeScriptは怒ってくれません。 is を使う関数の中身は危険性の塊であり、嘘をつかないことはプログラマの責任なのです。

function isStringArray ( obj : unknown ): obj is Array < string > { return Array . isArray ( obj ) && obj . every ( value => typeof value === " number " ); }

余談： unknown と as

実は、ここで出てきた unknown と as を併用することで any の必要性をほぼ消すことができます。

as は型を強制的に変えますが、全く無関係の型に変えることはできません。例えば "foo" as number のように文字列をそれとは無関係の数値型に変えることはできないのです（ここで無関係というのは、どちらの方向の部分型関係にもないことを意味します）。ただ、 unknown を間に挟んで "foo" as unknown as number とすれば通ります。 unknown を経由することで型を任意に変える（繰り返しますが、ランタイムには何も起こりません。あくまでTypeScriptを騙すだけです）ことができるのです。これができればだいたい any でやりたいことを達成できるため、実は as を優先して使うようにすれば本当に any を使う場面というのはほぼありません。

is の活用例

では、 is に話を戻してもうちょっと実際にありそうな活用例をご紹介します。

type MyObj = { name ?: string ; } // 渡されたオブジェクトたちの`name`プロパティの配列を返す。 // ただし`name`が存在しないものは抜かす。 function allNames ( objs : Array < MyObj > ): Array < string > { return objs . map ( obj => obj . name ). filter ( name => name != null ); } // ["Tanaka", "Yamada"] と表示される console . log ( allNames ([{ name : " Tanaka " }, {}, { name : " Yamada " }]));

このコードは型エラーとなります。なぜなら、 objs.map(obj => obj.name).filter(name => name != null) の型が Array<string> ではなく Array<string | undefined> と推論されるからです。まず objs.map(obj ==> obj.name) の型は Array<string | undefined> となります。これは想定通りですね。

次に .filter(name => name != null) で undefined を弾いているのだから、その結果は Array<string> となってくれるのが嬉しいですが、 filter はそこまで賢い推論をしてくれません。

この問題に対するひとつの対処法は as を使うことです。次のように、 as を使って Array<string | undefined> を Array<string> に強制的に修正するとエラーが消えます。

function allNames ( objs : Array < MyObj > ): Array < string > { return objs . map ( obj => obj . name ). filter ( name => name != null ) as Array < string > ; }

この方法の残念な点は、 as の危険性が allNames 全体に広がっている点です。実は、ここで is を使うともっと危険性を狭い範囲に押し込めることができます。そのためには次のようにします。

// 引数がnullでもundefinedでもないことをチェックする function isNotNull < T > ( x : T ): x is Exclude < T , null | undefined > { return x != null ; } // 渡されたオブジェクトたちの`name`プロパティの配列を返す。 // ただし`name`が存在しないものは抜かす。 function allNames ( objs : Array < MyObj > ): Array < string > { return objs . map ( obj => obj . name ). filter ( isNotNull ); }

新しい関数 isNotNull は is 型を使用しています。これは、返り値が true なら x が Exclude<T, null | undefined> 型であるという意味です。 T というのは引数 x の型であり、 Exclude<T, null | undefined> というのはざっくり言えば「 T から null と undefined の可能性を排除した型」です。例えば T が string | undefined の場合は Exclude<T, null | undefined> は string になります。

これを使う側は .filter(isNotNull) となりました。なんと、これでエラーが消えてしまいましたね。実は、 filter は返り値が is 型の関数を渡すとそれを理解して返り値の型を調整してくれるように定義されています。

今回のポイントは、従来は allNames の中全体に広がっていた危険性を is を使うことで isNotNull の中に押し込めることができた点です。 Array#filter を使っていて何か思い通りにいかないなあと思ったときは is のことを思い出してあげると解決するかもしれません。

TypeScriptの型を活用する

ところで、さっきの allNames はこんな書き方をすることもできます。

type MyObj = { name ?: string ; } // 渡されたオブジェクトたちの`name`プロパティの配列を返す。 // ただし`name`が存在しないものは抜かす。 function allNames ( objs : Array < MyObj > ): Array < string > { return objs . filter ( obj => obj . name != null ). map ( obj => obj . name ) as Array < string > ; } // ["Tanaka", "Yamada"] と表示される console . log ( allNames ([{ name : " Tanaka " }, {}, { name : " Yamada " }]));

すなわち、 map してから filter するのではなく filter してから map するようになりました。この場合もやはり is を使って as を消すことができます。

type MyObj = { name ?: string ; } type MyObjWithName { name : string ; } function hasName ( obj : MyObj ): obj is MyObjWithName { return obj . name != null ; } // 渡されたオブジェクトたちの`name`プロパティの配列を返す。 // ただし`name`が存在しないものは抜かす。 function allNames ( objs : Array < MyObj > ): Array < string > { return objs . filter ( hasName ). map ( obj => obj . name ) as Array < string > ; }

ここでは、 filter によって name を持たないオブジェクトが消えるという意図を型で表現するために MyObjWithName という型を作りました。やりたいことが型で明確に表されていて悪くないですが、ひとつ問題があります。それは、 MyObj と似た型である MyObjWithName をわざわざ再定義したくないということです。上の例ではまだいいですが、これが次のようになったらどうでしょうか。

type MyObj = { name ?: string ; phoneNumber : string ; address1 : string ; address2 : string ; address3 : string ; } type MyObjWithName { name : string ; phoneNumber : string ; address1 : string ; address2 : string ; address3 : string ; }

安全性のためとはいえこんなコードを書いてしまったらそれはそれで敗北という気がします。

実は、TypeScriptの強力な型システムを使えばこんなことをする必要はなくなります。この場合は以下のようにすればいいのです。

type MyObj = { name ?: string ; phoneNumber : string ; address1 : string ; address2 : string ; address3 : string ; } type MyObjWithName = MyObj & { name : string ; };

MyObjWithName 型を MyObj & { name: string; } 型として定義することができました。この & というのは交差型（intersection型）の構文であり、左右両方の型の条件を満たすような値の型を意味します。つまり、 MyObjWithName 型というのは、「 MyObj 型であり、しかも name プロパティが string 型である」という条件を表す型になります。これはちょうど我々がやりたいことに合致していますね。

このように、TypeScriptは既存の型をちょっといじった新しい型を作るのが得意です。これにより、同じ定義を何度も書くのを防ぐことができます。ここで紹介した交差型はその中でも簡単な部類であり、他にもmapped typeはconditional typeなどを使いこなせば型の表現力はぐんと上がるでしょう。これらの型について知りたい場合は以下の記事がおすすめです（宣伝）。

余談ですが、よりエクストリームな型を使って hasName を MyObj 専用ではなく一般化することもでき、こんな感じになります。

function hasName < T extends { name ?: unknown ; } > ( obj : T ): obj is ( T extends { name ?: infer U } ? T & { name : U } : never ) { return obj . name != null ; }

TypeScriptに勝利するためにここまでやる必要があるケースはあまり無いでしょうが、できると格好いいですね。まずこんなコードを（もちろんコメントで説明は書くべきですが）受け入れられるチームを組むところから始めないといけませんが。

まとめ

この記事では、TypeScript開発においてむやみに any や as などの危険な機能を濫用するコードを敗北者のTypeScriptと呼びその問題点を指摘しました。

これらの危険な機能を使うのはどうしても使う必要がある場合（TypeScriptに勝利した場合）に留めなければならず、その必要性は細心の注意をもって検討しなければいけません。検討とは、それがTypeScriptの敗北であることを明らかにすることです。そのためにはTypeScriptの限界を完全に理解し、危険な機能を使わずに済ますことが不可能であることを示さなければいけません。その壁を乗り越えて危険な機能を使用する場合も、記事中で説明したように、その危険性を最小限の範囲に閉じ込めるようにしなければいけません。

三度繰り返しますが、敗北者のTypeScriptも立派なTypeScript開発の一形態です。この記事はちゃんとTypeScriptを使えないならTypeScriptやめろといった主張をしたいわけではありません。ただ、あなた方は安全性を犠牲にしているということを伝えたいのです。

意図的に安全性を犠牲にしているなら、筆者はやめたほうがいいと思いますがとやかくは言いません。それよりも、自覚せずに安全性を破壊し、TypeScriptが提供してくれる安全性が空想上の存在となっているにも関わらずその存在を信じ続けているという事態は避けるべきです。

また、TypeScript信者の観点からは、敗北者のTypeScriptを書いている人がTypeScriptの安全性がこの程度であると見誤ってしまい、それがTypeScriptの限界であると思われることは避けたいところです。これが、この記事を書いた動機の一つです。

高いレベルの安全性を誰もが必要とするわけではないとしても、TypeScriptは“正しく”利用することで高い安全性を発揮することができる言語です。TypeScriptの型システムがこんなにエクストリームなのも、JavaScriptに型を付けて安全にするという難題に対してTypeScript開発チームが取り組んだ結果なのです。

ですから、この記事を読んでTypeScriptに勝利しようと思う方が一人でも増えることを願っています。