RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

レガシーなアプリケーションにこそTypeScriptを採用するべき4つの理由

f:id:tech-rakus:20200709095730p:plain

皆さんこんにちは。ラクスのフジサワです。
以前、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()は、bazundefinedなので、ランタイムエラーが発生します。
しかし、これは実行しなければ検出することができません。
では、これをTypeScript環境でコンパイルするとどうなるでしょうか?

コンパイルエラーが発生する

var baz = someFunc(-1);
console.log(baz.toString()); // コンパイルエラー:Object is possibly 'undefined'.

コンパイラがコードを解析して型推論した結果は次の通りとなり、numberまたはundefinedを返すメソッドであると認識されます。

function someFunc(foo: any): number | undefined

someFuncundefinedを返す可能性があるため、その返却値を代入している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.tscalc.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の型定義が導入された状態で、jQueryaddClassメソッドを記述してみましょう。

$(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型にのみ、nullundefinedの代入を許可する設定
    • null型:nullのみを許容する特殊な型
    • undefined型:undefinedのみを許容する特殊な型
  • 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の便利な機能について詳しく書かれているので、「完全に理解した」状態から「色々わかってないことに気づくフェーズ」にちょうどよい
    • 後半にReactやVue、Node.jsに導入する際の実践例の記載もあるので、名前の通り実践向き

実践TypeScript

実践TypeScript

  • TypeScript実践プログラミング
    • 上記2冊より広い分野の話が書かれており、知識の補完に便利でした

Copyright © RAKUS Co., Ltd. All rights reserved.