皆さんこんにちは。ラクスのフジサワです。
以前、TypeScriptを始める前は 「学習コストが高そう」「今動いているサービスに導入するのは難しいんだろうなあ」 というイメージが強かったのですが、なんてことはなく、タイトルにある通り、 「TypeScript使わないという選択肢なくね?むしろレガシーなアプリケーションこそ、使っていくべきじゃね?」 と手のひらがグリングリン回転したので、ぜひ皆さんに紹介させてもらいたいと思い、この記事を書くことにしました。
TypeScriptとは
- Microsoft製のAltJS(JavaScriptの代替となるもの)で、OSSで開発されている
- 静的型付き言語で、直感的にいえば「JavaScript+型」
- TypeScriptコンパイラや、webpack、BabelなどでJavaScriptにトランスパイルして使用する
- フロントエンド(ブラウザ)、バックエンド(Node.js)双方の開発で活用できる
理由① 『TypeScriptはJavaScriptのスーパーセットである』
これは、公式サイト の冒頭に出てくる言葉なのですが、ちょっと何言ってるか分からないですよね。
簡単にいうと、 「TypeScriptはJavaScriptの上位互換であり、JavaScriptの文法・知識がそのまま適用できるよ」 ということを言っています。
もちろん、TypeScript固有の言語仕様や、便利な使い方などは存在するのですが、それを習得していなくてもTypeScriptを利用することができます。
もっと言えば、「JavaScriptと思って使うこともできるし、必要に応じてTypeScriptならではの機能を使うこともできる」と表現できます。
理由② 型推論の恩恵により型に対する安全性を獲得できる
型推論とは、変数や関数シグネチャの型を明示的に宣言しなくても、初期化の際に代入する値や、関数呼び出しの実引数などの情報や文脈から、自動的に型を推測して決定する仕組みのことを言います。
これにより、いちいち型を書くと冗長な記述になりがちな型システムによる安全性を享受しながら、冗長さを回避したシンプルな記述ができると言われています。
※なお、型推論については賛否があり、型推論を活かしたコードは、明示的に型が表現されないので、予想外の推論結果になったり、逆にわかりにくくなってしまうという意見もあります。
TypeScriptの型推論がなぜレガシーコードにとって重要なのか?
前述の通り、TypeScriptの型推論の機能によって、既存のJavaScriptコードが型宣言を行っていなくても、型を推測し、コンパイル時に適切にチェックを行ってくれるようになります。
これは、「もともと型の概念が存在していないコード」であったとしても、部分的に型安全を保っていくことができるということを意味しています。
コードの具体例をいくつか挙げながら、どのように型推論が行われ、コンパイル時の型チェックに利用されるかを紹介します。
TypeScriptにおける型推論の例
例1 : let/const
let foo = 'hello'; // 明示的に指定していないが、fooはstringであると認識される foo = 1; // コンパイルエラー console.log(foo);
1行目でfooはstring
であることがわかっているので、2行目のnumber
型の値を代入しようとするとエラーになる
例2 : return値①
function someFunc() { return 100; // 明示的に指定していないが、返却値はnumberであると認識される } let message: string = someFunc(); // コンパイルエラー
someFuncの戻り値がnumber
であることが分かっているので、string
型の値への代入がコンパイルエラーになる
例3 : return値②
function multi(a: number, b:number) { return a * b; //number * number = number }
引数がnumber
なので、演算結果もnumber
であると推論される
例4 : 配列
let foo = [0, 1, 2] // foo: number[]と推論 let bar = [0, 1, 'aaa'] // bar: (number | string)と推論
例5 : Object
const user = { name: 'Taro', // name: stringと推論 age: 20 // age: numberと推論 } /* 以下の型を持つオブジェクトであることが推測される const user: { name: string; age: number; } */
JSON
let data = [ {"id" : 100, "name" : "Taro" }, {"id" : 101, "name" : "Hanako" } ] /* 以下の型を持つオブジェクトであることが推測される let data: { id: number; name: string; }[] */
レガシーコードが、TypeScriptの導入によって恩恵を得るサンプル
下記のようなコードがすでにシステム上に実装されているコードだとします。
function someFunc(foo) { if (foo > 0) { return foo * 2; } } var bar = someFunc(1); console.log(bar.toString());
もちろん、このJavaScriptは問題なく動作しますが、このコードには一つの問題が潜んでいます。
パラメータに負値を渡すとどうなるでしょうか。
var baz = someFunc(-1); // 負の値を渡すとundefinedが帰ってくる console.log(baz.toString());
baz.toString()
は、baz
がundefined
なので、ランタイムエラーが発生します。
しかし、これは実行しなければ検出することができません。
では、これをTypeScript環境でコンパイルするとどうなるでしょうか?
コンパイルエラーが発生する
var baz = someFunc(-1); console.log(baz.toString()); // コンパイルエラー:Object is possibly 'undefined'.
コンパイラがコードを解析して型推論した結果は次の通りとなり、number
またはundefined
を返すメソッドであると認識されます。
function someFunc(foo: any): number | undefined
someFunc
はundefined
を返す可能性があるため、その返却値を代入しているbaz
に対する操作が危険であることを、コンパイラが教えてくれるようになります。
someFuncを安全に使用するには、次のようなコードに修正する必要があります。
var bar = someFunc(-1); if (bar === undefined) { // undefinedのチェック bar = 0; } console.log(bar.toString());
上記のように記述すれば、コンパイルエラーは起きなくなります。
bar === undefined
のチェックがあることで、toString
が実行されるタイミングではnumber
しか到達しないことをTypeScriptが理解するようになるからです。
※なお、このような型の絞り込み処理を「型ガード」と呼びます。
先程はsomeFunc
を使う側のコードの問題をTypeScriptが検出してくれる例を見ましたが、実はsomeFunc
自体にも問題が潜んでいます。
コンパイル時のオプションnoImplicitReturns
を有効にすると、次のようなコンパイルエラーが発生する
function someFunc(foo) { // コンパイルエラー:Not all code paths return a value. if (foo > 0) { return foo * 2; } }
someFunc
がすべてのパスで返却値を返しておらず、意図しないundefinedを返す可能性を教えてくれるようになります。
このように、既存コードに型宣言など、型の情報を与えていないにもかかわらず、型推論によって未然に不具合を検出することができるようになったのが分かるかと思います。
理由③ JavaScriptライブラリとの連携が容易である
レガシーなシステムにはすでにたくさんのJavaScriptで記述されたライブラリが使用されています。
TypeScriptには、こうしたJavaScriptライブラリとうまく連携する仕組みが備わっています。
JavaScriptで記述されたライブラリをTypeScriptに導入する方法はいくつか存在します。
- 型宣言ファイルを自作する
- DefinitelyTypedで公開されている型定義ファイルを使用する
- allowJSオプションを活用する
型宣言ファイル(d.tsファイル)を自作する方法
「型宣言ファイル」を作成し、TypeScriptにライブラリの型情報を伝えることで実現する方法です。
例えば、次のようなJavaScriptで記述されたライブラリcalc.js
があるとします。
// calc.js exports.sub = function(a, b) { return a - b; }
このライブラリを利用するには、次のような型宣言ファイルを作成して配置することで実現可能です。
※今回の例では、tsファイルと同じディレクトリに配置するようにしています。
// calc.d.ts export function sub(a: number, b: number): number;
これにより、exec.ts
はcalc.d.ts
を通してsub
関数を認識することができ、コンパイルが通るようになります。
// exec.ts import { sub } from "./calc"; console.log(sub(100, 1));
型宣言ファイルの定義に基づき、コンパイル時に型推論や型チェックが行われるようになります。
もちろん、IDEにも認識されるのでコード補完なども有効になります。
DefinitelyTyped(@types)で公開されている型定義ファイルを使用する方法
jQueryやChart.jsといったライブラリをTypeScriptに導入する際にも、この「型宣言ファイル」が必要になります。
しかし、規模の大きなライブラリに対して、先ほどの例のようにこれを自分で作成するのは骨が折れる作業です。
これに対し、DefinitelyTyped(http://definitelytyped.org/)というコミュニティプロジェクトが存在しており、ここにはJavaScriptで記述されたライブラリの型宣言ファイルが集まっています。
※あくまでコミュニティによるOSSなので、最新バージョンに追従していない、正しくないという場合も無くはないので注意が必要です。
DefinitelyTypedに対象のライブラリの型宣言ファイルが存在するかは、TypeSearch(https://microsoft.github.io/TypeSearch/) で検索することができます。
DefinitelyTypedに登録されている型宣言ファイルは、npmコマンドでプロジェクトに取り込むことができます。
npm install --save @types/jquery
jQueryの型定義が導入された状態で、jQueryのaddClass
メソッドを記述してみましょう。
$(function() { $('#foo').addClass('bar'); });
TypeScriptがjQueryのメソッドを認識しているので、上記のコードはコンパイルエラーにはなりません。
では、ここで、addClass
の引数に数値を渡してみましょう。
$(function() { $('#foo').addClass(1); });
すると、下記のようなコンパイルエラーが発生するようになります。
Argument of type '1' is not assignable to parameter of type 'string | string[] | ((this: HTMLElement, index: number, currentClassName: string) => string)'.
上記から、jQueryが提供する関数の型定義が正しく認識されていることがわかります。
allowJSオプションを使用する方法
JavaScriptで記述されたライブラリをTypeScriptに導入するには、allowJS
オプションを有効にする方法もあります。
※通常は.ts拡張子のみコンパイルされ、.js拡張子のファイルはコンパイルの対象にはなりませんが、このオプションを有効にすることで、.jsファイルもコンパイルの対象にすることができます。
$ tsc --allowJS calc.js exec.ts
ただし、素の状態のJavaScriptは型の情報が与えられていないため、かなり緩い型推論結果になることに注意してください。
// calc.js exports.sub = function(a, b) { return a - b; } /* 推論されたシグネチャ sub(a: any, b: any): number */
しかしながら、もともと型の概念を持っていなかったレガシーなライブラリが、「ただ読み込ませただけ」で、多少なりとも型による安全性の能力を手に入れることができていることが注目すべきポイントです。
JSDocによって型定義をすることも可能
下記のようにJSDocを記述すると、詳細に型の定義を行うことができます。
/** * @param {number} a * @param {number} b * @return number */ exports.sub = function(a, b) { return a - b; } /* 推論されたシグネチャ sub(a: number, b: number): number */
ただ読み込ませるだけで型推論の恩恵を得ることができる、だけでも十分であることはすでに述べた通りですが、既存プロジェクトがきっちりとJSDocが記述している環境であれば、TypeScriptへの移行はなおさら恩恵を得やすいと言えるでしょう。
どの方法を採用すれば良いのか?
ここまで紹介した、既存のJSライブラリをTypeScript環境に導入する方法は、それぞれにメリット・デメリットがあるので、プロジェクトの状況に応じて使い分けるのが良いと思います。
型宣言ファイルについては、ライブラリ自身が型宣言ファイルを公開している場合もあります。
まずはライブラリ公式、もしくはDefinitelyTyped上のものを探し、存在していない場合はallowJSオプションを使用して、徐々に型宣言ファイルを作る、もしくはライブラリ側をTypeScript仕様に変える、というアプローチが良いのではないでしょうか。
理由④ 段階的なTypeScript化を助けるコンパイルオプションがある
TypeScriptには、コンパイル時のチェック内容を柔軟に変えることができるオプションが備わっています。
これらを使用することで、プロジェクトの状況に合わせたTypeScript導入を行うことができます。
noImplicitAny
- 暗黙的に型推論の結果
any
型となった場合にコンパイルエラーにするオプション - OFFにすることで、暗黙的な
any
を許容できる - 明示的に型指定を行わない場合、メソッドの引数などは
any
型になる any
型は、その名前の通り「なんでもあり」の型なので、あまり良いものではなく、any
型を避けるのが望ましい- ただし、もともとJavaScriptで記述されているコードについては、一旦このオプションをOFFにしておいて、コンパイルエラーを目印に、徐々に型指定を増やしていくのが良いと思われる
strictNullChecks
- null型、undefined型にのみ、
null
、undefined
の代入を許可する設定- null型:
null
のみを許容する特殊な型 - undefined型:
undefined
のみを許容する特殊な型
- null型:
- T | null 型と null 型は別物と扱われ、T | null 型には
null
を代入できない - これにより、意図しない
undefined
や、意図しないnull
を防ぐことができる - noImplicitAny同様、最初はオプションをOFFにしておき、ONにした時に見つかるコンパイルエラーを徐々に修正すると良いと思われる
ずさんなコードの匂いを検出してくれる便利なコンパイルオプション
- noUnusedLocals
- 未使用のローカル変数があるとエラーになる
- noUnusedParameters
- 未使用の引数があるとエラーになる
- noImplicitReturns
- 既出。メソッド内の全てのパスでreturn文が無い場合エラーになる
- noFallthroughCasesInSwitch
- Switch文内におけるbreak漏れがエラーになる
まとめ:TypeScriptは、JavaScriptプロジェクトからうまく移行する環境が整っている
TypeScriptはJavaScriptのスーパーセットなので、既存のコードをそのまま使うことができる
型推論によって、ただのJavaScriptが型の力を手に入れることができる
JavaScriptで記述された既存資産を柔軟に組み入れる仕組みやそれを支えるコミュニティが存在する
柔軟なコンパイルオプションによって、レガシーなコードを段階的に厳密なコードに変えていくことができる
おわりに
TypeScriptの導入がいかに容易で、かつどれだけメリットがあるかをこの記事で紹介させて頂きました。
この記事をご覧になった方で、TypeScript導入に及び腰の方がいらっしゃいましたら、ぜひトライしてみて頂ければ幸いです!
参考文献
本記事執筆にあたり、私が学習に使用した書籍を下記に紹介します。
速習TypeScript
- まずTypeScriptを「完全に理解した」状態にするのにおすすめ
実践TypeScript
- 型やTypeScriptの便利な機能について詳しく書かれているので、「完全に理解した」状態から「色々わかってないことに気づくフェーズ」にちょうどよい
- 後半にReactやVue、Node.jsに導入する際の実践例の記載もあるので、名前の通り実践向き
- TypeScript実践プログラミング
- 上記2冊より広い分野の話が書かれており、知識の補完に便利でした
エンジニア中途採用サイト
ラクスでは、エンジニア・デザイナーの中途採用を積極的に行っております!
ご興味ありましたら是非ご確認をお願いします。
https://career-recruit.rakus.co.jp/career_engineer/カジュアル面談お申込みフォーム
どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。
以下フォームよりお申込みください。
forms.gleイベント情報
会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! rakus.connpass.com