TypeScript再入門 ― 「がんばらないTypeScript」で、JavaScriptを“柔らかい”静的型付き言語に

JavaScriptプロジェクトでTypeScriptを導入する際には、“柔らかい”静的型付き言語とするのがおすすめです。藤吾郎(gfx)さんがまとめた「がんばらないTypeScript」のガイドラインです。

TypeScript再入門 ― 「がんばらないTypeScript」で、JavaScriptを“柔らかい”静的型付き言語に

TypeScriptは、すべてのJavaScriptプロジェクトで採用する価値のある技術です。TypeScriptとこれに対応したエディタを導入することで、補完や型ベースの整合性のチェックにより、すべてのプロジェクトで生産性が上がります。またリファクタリングも容易になるので、長期あるいは大規模なプロジェクトでも品質を保ちやすくなります。

この記事では、TypeScriptについて最低限の知識とともに、サクッと(どちらかというと既存のプロジェクトに)導入するためのノウハウや手順を、筆者@__gfx__の経験もあわせて解説していきます。

ただし、TypeScriptのユースケースは、Webフロントエンド、モバイルアプリ、サーバーサイドアプリなど多岐にわたります。そこでこの記事では、以下のようなプロジェクトを想定して、設定などを具体的に見ていきます。

  • Webフロントエンドのビルドにwebpackを使う
  • ES modulesまたはCommonJS準拠のモジュールシステムを使う

※本記事は、TypeScript v3.4(2019年3月リリース)に基づいています。

TypeScriptとは何か? がんばらないTypeScriptとは何か?

あらためて「TypeScriptとは何か?」から始めましょう。TypeScriptは、静的型付きプログラミング言語です。特徴としては、ほとんどの言語機能をJavaScriptから継承していることです。TypeScriptのコードはJavaScriptに変換(コンパイル1され、最終的にJavaScriptエンジンで実行されます。

これは重要なことなので何度も繰り返しますが、実際、TypeScriptはほとんどJavaScriptです。例えば、公式サイトのうたい文句は“JavaScript that scales”です。“scales”というのは、ここでは「大規模開発に耐えられる」といった意味ですから、「大規模開発に耐えられるJavaScript」ということです。

JavaScriptにコンパイルされる言語はたくさんありますが、TypeScriptはコンパイル前と後のコードにほとんど違いがありません。「TypeScriptはほとんどJavaScriptである」というのは文字通りの意味で、JavaScriptの知識はすべてそのままTypeScriptに応用できます。

これに対して、CoffeeScriptやDartといったJavaSciptと大きく異なる言語機能を持つaltJSでは、コンパイル後のコードは、元のコードとも自然なJavaScriptとも異なるものになります。その良し悪しはともかく、JavaScriptと異なる言語機能を持つこれらのaltJSには、JavaScriptとは異なる知識体系が必要ということです。

ところで、一般的には、静的型付き言語は動的型付き言語より高速であるといわれます。つまり、静的な型(型注釈)はCPUのための型でもあると考えられています。しかし、TypeScriptはJavaScriptと実行速度が変わりません。つまり、TypeScriptの型注釈は、人にとってはコメントであり、またエディタなどのツールにとっては補完などのコーディングサポートに使われる情報でもありますが、実行には何の影響もないのです。

TypeScript導入の可否

ところで2019年のいま、TypeScriptは導入すべきでしょうか。これについては「導入すれば得るものが多い」という評価がほぼ定まっていると考えてよいでしょう。「『TypeScriptを導入すべきか』で悩む時代は既に終わっている」という意見もあります。

JavaScriptプロジェクトへの導入も、コンパイルを通すまでなら、早ければ数分で済みます。一通り動作確認して完成させるまででも、数日程度で済むはずです。

試しに、この記事のためReact公式サイトで紹介されているReactアプリをいくつかTypeScript化してみました。作業時間としては、インストールの待ち時間を除けば、それぞれ10分程度で変換して動作確認までできました。

アプリ 差分(プルリクエスト) wc -l
Calculator gfx/calculator/pull/1 461行
Pokedex gfx/pokedex/pull/1 267行

もっとも、多少のハマりどころがないわけではありません。特に、TypeScriptを導入する際の設定は知識が必要です。また、どのようにTypeScriptが実行されるかという一連の流れの理解につまずく人が多いようです。本記事では、そういったハマりどころを集中して解説していこうと思います。

難しい型システムをマスターしなくてもTypeScriptで開発できる

TypeScriptは、JavaScriptに静的型という大いなる力を与える言語です。そしてその「静的型の堅さ」もかなり自由に設定できます。strictにするオプションをすべて有効にして「堅い」静的型付き言語として振る舞うこともできれば、その逆にJavaScriptに毛が生えた程度の「柔らかい」静的型付き言語として振る舞えもします。

ところで、TypeScriptは型や設定が複雑で難しく学習コストが高いという意見をよく目にします。実際のところ、TypeScriptの型システムをマスターするのが難しいというのは事実です。設定ファイル(tsconfig.json)の項目も多く、しかも「静的型の堅さ」について最初から適切な判断をしなければならないような気がして、敷居が高く感じてしまうのも仕方がありません。

しかし、型システムをマスターしなければTypeScriptで開発できないわけではありません。複雑な型演算2は、アプリケーションを書くときに必要になることはほとんどありません。またほかの多くの静的型付き言語と異なり、型エラーで困ったときはいつでもanyで握りつぶせます。動くことが分かっているコードで遭遇する型エラーを握りつぶすのは、特にJavaScriptをTypeScriptに置き換えるフェーズでは、ためらわずにやる方がよいと思っています。

設定ファイルについても同じように、最初からすべての型チェックのオプションを有効にするべきとは思いません。いくつかの型チェックは、少なくともTypeScriptに不慣れな間は、無効にする方が生産性が上がるはずです。

「がんばらないTypeScript」というガイドライン

この記事では、以下のガイドラインに適合するTypeScriptのスタイルを「がんばらないTypeScript」スタイルと定義します。

設定ファイルをがんばらない
  • tsconfig.jsonではオプトインの型チェックオプションを任意で無効化してよいものとする
  • 特にnoImplicityAnyは無効化を奨励する
型付けをがんばらない
  • 型注釈はあとから足せばよいと割り切る
  • コードにおいてはいつでもanyで型エラーを握りつぶしてよい
型定義ファイルをがんばらない
  • 型定義の提供されていないライブラリは型がないまま使う
  • DefinitelyTyped@typesはしばしば「まともなものがあればラッキー」くらいに割り切る

それぞれについての詳細は後述します。

この「がんばらない」は、「自分でもがんばらないし、チームメイトにもがんばりを求めない」、例えばコードレビューでは厳密な型注釈を求めないということです。チームで運用するとなれば、そのチームでコンセンサスを得る必要もあるでしょう。

「がんばらないTypeScript」は、TypeScriptの設定や型の厳格な運用に時間を使うのではなく、プロダクトの開発に時間を使うべきという思想です。TypeScriptに慣れていないときは特にそうで、型付けは慣れてからする方がずっと少ない時間で正確にできるはずです。

そして、たとえTypeScriptについてがんばらないとしても、TypeScriptには型推論があるため型チェックよる恩恵は十分あります。TypeScriptの恩恵をほどほどに得つつプロダクト開発の生産性を上げていこう、というのが「がんばらないTypeScript」の目指す道です。

なお、この「がんばらないTypeScript」というガイドラインの名称は、@t_snzkさんのブログ記事から拝借しました。この記事の「がんばらないTypeScript」の定義も、基本的にはこのエントリのものと互換性があります。

「がんばらないTypeScript」で得られるもの

ところで「がんばらないTypeScript」の話をすると、よく「それではTypeScriptを導入する意味がないのではないか」といわれます。しかし、もちろんそんなことはありません。

TypeScriptは、すべてのstrict系オプションを無効にした状態でさえ、JavaScriptよりはずっと厳格です。例えば、以下のコードはJavaScriptでは実行可能で無意味な値(NaN)を出力しますが、TypeScriptだと最も緩いデフォルトの設定でもコンパイルは通りません。

// foo-increments.ts
let foo = "foo";
foo++;
console.log(foo);

コンパイルすると、次のようなエラーになります。

$ npx ts-node foo-increments.ts
⨯ Unable to compile TypeScript:
add.ts:4:1 - error TS2356: An arithmetic operand must be of type 'any', 'number', 'bigint' or an enum type.

4 foo++;
  ~~~

この出力は「数値演算はanynumberbigintまたはenum typeに対してのみ行えます」という意味です。

これは、TypeScriptだとlet foo = "foo"と宣言したとき変数fooが型推論によって文字列型になり、文字列型にインクリメント演算子(++)は適用できないためです。

このほか、ECMAScriptの標準ライブラリの型定義や標準DOM APIの型定義がもともと提供されていることもあり、「がんばらないTypeScript」の設定のもとで必須でない型注釈をすべて省略したとしても、組み込み型のための型チェックとコード補完により、JavaScriptよりはるかに開発しやすいことでしょう。

もちろん、TypeScriptに習熟するにつれて、徐々に厳格にしていくことは可能ですし、そうした方がよいとは思います。しかし、最初からTypeScriptをマスターする必要はありませんし、またマスターしていなくても、型チェックの恩恵は十分にあります。

TypeScriptによるWebアプリケーションの開発

それでは実際に、TypeScriptの設定を見てみます。最初に説明したように、次のような条件のWebアプリを想定しています。

  • Webフロントエンドのビルドにwebpackを使う
  • ES modulesまたはCommonJS準拠のモジュールシステムを使う

これらに必要なツールチェインとTypeScriptの考え方を見ていきます。

TypeScriptのコンパイラ

TypeScriptのコンパイラは、npmモジュールとして配布されています。TypeScriptコンパイラはTypeScriptで書かれており、TypeScript APIを使ったサードパーティ製のカスタムコンパイラも数多くあります。

このモジュールにはコンパイラであるtscコマンドと、language serviceを提供するtsserverコマンドが入っています。tsserverはエディタなどのためのlanguage service3を利用するツールが使うものです。

また、ちょっとしたTypeScriptコードをすぐ試したいときやテストの実行などに便利なts-nodeというコマンドがnpmモジュールとして提供されています。ts-nodeはTypeScriptのカスタムコンパイラです。

2 ts-node - npm

使用例は次のようになります。

$ npx ts-node -e 'let a = "foo"; a++'
[eval].ts:1:16 - error TS2356: An arithmetic operand must be of type 'any', 'number', 'bigint' or an enum type.

1 let a = "foo"; a++
                 ~

webpackでTypeScriptを使うときは、ts-loaderを利用します。これもTypeScript APIを使ったカスタムコンパイラです。

3 ts-loader - npm

エディタはlanguage serviceをサポートしたものを

エディタは、必ずTypeScriptのlanguage serviceをサポートしたものを使ってください。TypeScriptのlanguage serviceは非常に強力なので、これが使えないとTypeScriptを使う利点は半減します。

おすすめは、Visual Studio Code(vscode)です。

筆者は、RubyMine(IntelliJ IDEA系列のIDE)もよく使います。

このいずれも追加のプラグインなしで、TypeScriptのlanguage serviceを利用できます。

シンプルなTypeScript環境でスクリプトを実行してみる

webpackの設定に入る前に、まずシンプルなTypeScript実行環境を作って試しましょう。

手順は次のようになります。リポジトリ名はhello-typescriptです。

mkdir hello-typescript
cd hello-typescript
npm init -y
npm add -D typescript ts-node @types/node core-js
npx tsc --init
echo 'node_modules/\n*.js\npackage-lock.json' > .gitignore
git init && git add . && git commit -m "initial commit"

tsc --initは、TypeScriptの設定ファイルであるtsconfig.jsonを生成します。

このデフォルトで生成されるtsconfig.json自体、コメントが豊富で4、情報量が多いのですが、載っていない重要なオプションもあるので、TypeScriptのコンパイラオプションも眺めるとよいでしょう。TypeScriptのアップデートに伴いtsc --initの出力もアップデートされてきているため、新しいプロジェクトを始めるときはtsc --initを確認するといいでしょう。

これでひとまず環境はで出来上がります。次のようなhello.tsを用意して(これはJavaScriptとしても妥当なスクリプトです)

// hello.ts
console.log("Hello, TypeScript!");

ts-nodeで実行してみましょう。

$ npx ts-node hello.ts
Hello, TypeScript!

簡単ですね。

とはいえ、ここにES modules/CommonJSなどで外部モジュールを読み込むと少し複雑になります。

例えば、次のようなNodeJSのfsモジュールを使うスクリプトを考えます。

import * as fs from "fs";
// このファイル自身を読み込んで表示する
console.log(fs.readFileSync(__filename).toString());

このときimport文やrequire式は、TypeScriptとNodeJSそれぞれで独立して評価されます。つまり、TypeScript自身もモジュールを探して(module resolution)、存在しなければコンパイルエラーにします。

ここでは、@types/nodeという型定義モジュールが、fsモジュールの型定義やグローバル変数__filenameを提供します。試しにnpm remove @types/nodeをしたあとnpx tscとコンパイルだけをしてみると、コンパイルエラーになはずですnpm install -D @types/nodeで戻せるので気軽にどうぞ!)

「がんばらないTypeScript」の設定と実行

次に、「がんばらないTypeScript」を含めたTypeScriptのおすすめの設定を紹介します。先ほど生成したデフォルトのtsconfig.jsonを、次のように編集してください。

diff --git a/tsconfig.json b/tsconfig.json
index ec795e8..02817b0 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -3,33 +3,33 @@
     /* Basic Options */
     "target": "es5",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
     "module": "commonjs",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
-    // "lib": [],                             /* Specify library files to be included in the compilation. */
+    "lib": ["es2019"],                             /* Specify library files to be included in the compilation. */
     // "allowJs": true,                       /* Allow javascript files to be compiled. */
     // "checkJs": true,                       /* Report errors in .js files. */
-    // "jsx": "preserve",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
+    "jsx": "react",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
     // "declaration": true,                   /* Generates corresponding '.d.ts' file. */
     // "declarationMap": true,                /* Generates a sourcemap for each corresponding '.d.ts' file. */
-    // "sourceMap": true,                     /* Generates corresponding '.map' file. */
+    "sourceMap": true,                     /* Generates corresponding '.map' file. */
     // "outFile": "./",                       /* Concatenate and emit output to single file. */
-    // "outDir": "./",                        /* Redirect output structure to the directory. */
+    "outDir": "./build",                        /* Redirect output structure to the directory. */
     // "rootDir": "./",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
     // "composite": true,                     /* Enable project compilation */
-    // "incremental": true,                   /* Enable incremental compilation */
+    "incremental": true,                   /* Enable incremental compilation */
     // "tsBuildInfoFile": "./",               /* Specify file to store incremental compilation information */
-    // "removeComments": true,                /* Do not emit comments to output. */
+    "removeComments": false,                /* Do not emit comments to output. */
     // "noEmit": true,                        /* Do not emit outputs. */
     // "importHelpers": true,                 /* Import emit helpers from 'tslib'. */
-    // "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
+    "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
     // "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */

     /* Strict Type-Checking Options */
     "strict": true,                           /* Enable all strict type-checking options. */
-    // "noImplicitAny": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */
-    // "strictNullChecks": true,              /* Enable strict null checks. */
-    // "strictFunctionTypes": true,           /* Enable strict checking of function types. */
-    // "strictBindCallApply": true,           /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
-    // "strictPropertyInitialization": true,  /* Enable strict checking of property initialization in classes. */
-    // "noImplicitThis": true,                /* Raise error on 'this' expressions with an implied 'any' type. */
+    "noImplicitAny": false,                 /* Raise error on expressions and declarations with an implied 'any' type. */
+    "strictNullChecks": false,              /* Enable strict null checks. */
+    "strictFunctionTypes": false,           /* Enable strict checking of function types. */
+    "strictBindCallApply": false,           /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
+     "strictPropertyInitialization": false,  /* Enable strict checking of property initialization in classes. */
+    "noImplicitThis": false,                /* Raise error on 'this' expressions with an implied 'any' type. */
     // "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */

     /* Additional Checks */
@@ -39,7 +39,8 @@
     // "noFallthroughCasesInSwitch": true,    /* Report errors for fallthrough cases in switch statement. */

     /* Module Resolution Options */
-    // "moduleResolution": "node",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
+    "moduleResolution": "node",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
+    "forceConsistentCasingInFileNames": true,
     // "baseUrl": "./",                       /* Base directory to resolve non-absolute module names. */
     // "paths": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
     // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */

いずれも重要なのでひとつひとつ解説します。

がんばらないことと関係のないおすすめ設定

まず「がんばらないTypeScript」スタイルと関係のない設定から説明します。

+ "lib": ["es2019"]

これは、TypeScriptコンパイラに同梱されているライブラリでどれを使うかを指定します。ブラウザ用のコードであれば["es2019", "dom", "dom.iterable"]、NodeJS用のコードであれば["es2019"]などにするのが定番でしょう。

+ "jsx": "react",

React/JSXを使うときは"react"にしてください。使わない場合はコメントアウトしたままでかまいません。なお、TypeScriptでJSX構文を使う場合は必ず拡張子を.tsxにする必要があります。

ちなみに筆者はviewライブラリのなかではReact推しです。これはReact自体が強力であることもさることながら、TypeScript処理系によるサポートが非常に強力だからです。

+ "sourceMap": true,

ソースマップの出力を有効にします。デバッガやスタックトレースでコンパイル前のTypeScriptのファイル名や行番号を表示するために必要です。

+ "outDir": "./build",

TypeScriptコンパイラは、デフォルトでソースファイルと同じディレクトリにコンパイル後のJavaScriptファイルを出力します。このデフォルト設定だとディレクトリが不必要に散らかるため、buildディレクトリに出力するようにします5

+ "incremental": true,

差分ビルドを有効にしてコンパイルを高速化します。TypeScript v3.4で追加された新しいオプションで、まだ枯れているとはいえないものの、ビルドはかなり高速になります。

+ "removeComments": false,

コメントを残します。webpackを使う場合は、code splittingを制御するコメントを残す必要があるためです。

+ "downlevelIteration": true,

コンパイルターゲットがES5のとき、for-of構文を使うために必要です。

+ "moduleResolution": "node",

TypeScriptコンパイラが行うモジュールの検索をNodeJS互換にします。

+ "forceConsistentCasingInFileNames": true,

ファイル名の大文字小文字に一貫性がないときにコンパイルエラーにします。大文字小文字を区別しないファイルシステムを使っているときはこのオプションを有効にした方がよいでしょう。

がんばらないための設定の詳細

次に、「がんばらないTypeScript」スタイルの「柔らかい」設定です。

+ "noImplicitAny": false,
+ "strictNullChecks": false,
+ "strictFunctionTypes": false,
+ "strictBindCallApply": false,
+ "strictPropertyInitialization": false,
+ "noImplicitThis": false,

JavaScriptプロジェクトをTypeScriptに変換する場合、これらの設定はいったん無効にしておいて、変換作業を終えてから第二フェーズとして有効にすることをおすすめします。これらのオプションがすべて無効だとしても、JavaScriptよりはかなり「堅」く、型チェックやエディタによるサポートは受けられます。

TypeScriptを導入してしばらくして慣れてきたら、noImplicitAny以外のオプションを有効にするとより堅牢になります。noImplicitAnyは、新しくプロジェクトを始めるときには有効にするメリットがありますが、途中からTypeScriptに変換する場合には作業量に対して得るものが少ないのではないかと思っています。

筆者が関わっているKibelaも、途中でTypeScriptに変換したプロジェクトで、noImplicitAnyは無効のままです。有効にする予定もありません。それ以外のstrict系オプションは有効にしてありますが、それらはひとつひとつ様子を見ながら有効にしていきました。

JavaScriptプロジェクトをTypeScriptに変換する

JavaScriptプロジェクトにTypeScriptを導入するときは、次のようなプロセスで行います。

もともとES2015+で書いているJavaScriptプロジェクトで、コードが数千行から数万行程度のプロジェクトであれば、すべてのファイルを一括してTypeScript化した方がよいでしょう。

  1. TypeScriptをインストールしてtsconfig.jsonを用意する
  2. 拡張子を変更する
  3. コンパイルエラーをひたすら修正する

順に説明していきます。TypeScriptのインストールと設定は、すでに解説した通りです。ほとんどのケースで型定義モジュールはなくてかまいませんが、Reactを使う場合は@types/react@types/react-domを使った方がよいでしょう。

拡張子の変更では、すべての.js/.jsxファイルを.ts/.tsxにリネームします。renameコマンドを使うと簡単です。拡張子.jsでJSXを使っている場合は、無条件にすべて.tsxにしてしまう方がよいでしょう。

rename 's/\.js\z/.tsx/' src/**/*.js

コンパイルエラーの修正では、npx tsc --watchでTypeScriptコンパイラのプロセスを常駐させます。いくつかのエラーは、次のように機械的に修正できます。

perl -i -pe 's/\bextends React\.(Pure)?Component /extends React.${1}Component<any, any> /' src/**/*.tsx

変換作業はとにかく素早く終えることが重要なので、いったん何も考えずanyを宣言したりanyにキャストしたりして、とにかくコンパイルを通しましょう。

コンパイルエラーが解決できたら、ほぼ終わったようなものです。

あとはts-loaderのドキュメントにあるように、webpackの設定に次のようなextensionとloader ruleを足します。

# .tsと.tsxがwebpackに読み込まれるようにする
+ extensions: [".mjs", ".js", ".jsx", ".ts", ".tsx", ".json"],
module: {
  rules: [
+     {
+       test: /\.(?:ts|tsx)$/,
+       exclude: /node_modules/,
+       use: {
+         loader: "ts-loader",
+       },
+     },
  ]
}

これで動作確認ができるはずです。

最後に、テストファイルも同様にTypeScriptに変換して、動作確認をしたら終了です。テストの実行は、それぞれのテストフレームワークのTypeScript対応を参照してください。

また、すでに一度紹介済みですが、既存のJavaScriptプロジェクトをTypeScriptに変換したサンプルを用意したのでこちらも参照してみてください。

プログラミング言語としてのTypeScript

最後に、プログラミング言語としてのTypeScriptを少しだけ眺めてみます。

JavaScriptとの違い

JavaScriptからTypeScriptへの変換プロセスを見ると分かる通り、JavaScriptをTypeScriptに変換するにあたって必要なのは、ほとんど型宣言だけです。

型システムは少し学ぶ必要がありますが、「使う」だけならすぐ使えるはずです。

型コンテキスト

TypeScriptの型に関する構文を理解するにあたって重要な概念が「型コンテキスト」です。

型コンテキストは、TypeScriptの構文においてトークンや式が型として評価される場所を指します。これは公式に定義された用語ではありませんが、意識できると理解が簡単になるはずです。

例えば、typeof expr演算子(type query)は、通常は"string"などの文字列を返します。これに対してtypeof exprを型コンテキストで使うと、exprの型を返す型演算子となります。

例えば、次のようにadd()関数を宣言します。

function add(a: number, b: number): number {
    return a + b;
}

このadd()関数の型は(a: number, b: number) => numberです。typeof add演算子の値を参照すると(つまり「値コンテキスト」で評価すると)"function"という文字列値を返します。まったく同じtypeof addという式を「型コンテキスト」で評価すると、その型である(a: number, b: number) => numberが得られます。

// 「値コンテキスト」だと値を返す
const v = typeof add; // => "function"

// 「型コンテキスト」では型を返す
type f = typeof add; // => (a: number, b: number) => number

なお、TypeScriptの型を文字列化する標準的な方法がないので、fの定義はエディタで見るしかありません。Typecript playgroundでfにカーソルを合わせるか、IntelliJ IDEAでcommandキー(macOSの場合)を押しながらfにカーソルを合わせると、これが確認できます。

型コンテキストはさらに、nullundefined、数値リテラル、文字列リテラル、真理値リテラル、そしてそれらのリテラルを要素とする配列リテラルとオブジェクトリテラルが使えます。型コンテキストとして評価する箇所は決まっているので、慣れると自然に読めるようになるとは思いますが、最初は少し引っかかるかもしれません。

構造的部分型

TypeScriptの型は、「同じインターフェイス(メソッドとプロパティ)があれば同じ型である」という構造的部分型という思想を採用しています。

例えば、ReadonlyArrayというArray型のサブタイプ(インターフェイスがある型のサブセットである型)は、TypeScriptで定義された、ただのinterface型です。

// https://github.com/Microsoft/TypeScript/blob/master/lib/lib.es5.d.ts
interface ReadonlyArray<T> {
  // Array<T> のlengthはreadonlyではないが、ここではreadonly
  readonly length: number;
}

これでも、例えば次のような、ArrayからReadonlyArrayへの暗黙の型変換が可能なのです。

const a: RedonlyArray<string> = new Array<string>()

構造的部分型は、すべての型について適用されます。Arrayなどのビルトイン型や、HTMLElementなどのDOM APIも例外ではありません。

このルールは「すべての型は、型コンテキストではinterfaceとして扱われる」とも考えられます。これにより、例えばクラスをinterfaceとして実装することすら可能です。

// クラスを継承する。これはJavaScript (ES2015+) と同じ意味
class MyStringA extends String {
  // Stringから実装を継承する
}

// クラスをinterfaceとして実装する
class MyStringB implements String {
    // Stringから実装を継承しない
    // つまりStringが提供するインターフェイスをすべて実装しなければならない
}

ジェネリクス

ところでArray<T>のように型パラメータを<...>で受けとる機能を、ジェネリクスといいます。Arrayの場合は、「Array<string>(文字列の配列)」と「Array<number>(数値の配列)」などを型として区別したいからです。

この型パラメータの中身は、型コンテキストで評価されます。

ジェネリクスは型をパラメータとして受けとり、新しい別の型を生成する「型関数」としての側面もあります。例えば、Readonly<T>はTypeScript組み込みの型関数で、Tという型のプロパティすべてをreadonlyとして宣言しなおした新しい型を返します。

// xとyというプロパティを持つオブジェクト型を宣言する
type Point = {
  x: number;
  y: number;
};

// RedonlyPointは { readonly x: number; readonly y: number } と同じ
type ReadonlyPoint = Readonly<Point>;

ジェネリクスや型関数による型演算は強力ですし、使いこなすと便利ではあります。しかし、これらはコンパイルの型チェックを強化するための機能で、コンパイルの後のコードの実行には何の影響もありません。これらとは、そのような意識で付き合うといいと思っています。

まとめ

TypeScriptについて、導入に際してハマりがちな設定と、TypeScript言語自体に対する考え方を紹介しました。

TypeScriptは奥が深いプログラミング言語ですが、とはいえ言語仕様自体はJavaScriptとほとんど同じです。それほど習熟していなくても、使い始めるのは簡単です。このままコンパイラとエコシステムが成長し続ければ、今後ますます重要な技術となっていくでしょう。

それでは、よいTypeScriptライフを!

藤 吾郎(ふじ・ごろう) 4 @__gfx__ / 5 gfx

6
株式会社ディー・エヌ・エーやクックパッド株式会社でのエンジニア経験を経て、2016年8月よりビットジャーニーに入社。 Webやモバイル技術のエコシステムに関心がある。近年はAndroid関連でツールやライブラリをOSSとして多数公開し、DroidKaigiの運営スタッフも務める。
ブログ:Islands in the byte stream

  1. TypeScriptをJavaScriptに変換すること。「トランスパイル」ともいいますが、公式ドキュメントでは一貫してコンパイルと書いてあるので本記事でもコンパイルということにします。

  2. コンパイル時にある型から別の新しい型を生成することを「型演算」といいます。

  3. language serviceは、構文ハイライト、リアルタイム型チェック、リファクタなどのエディタ用の各種サービスです。

  4. 本来JSONファイルはコメントが存在しないので、.jsonといいつつ実際にはコメントを許すように拡張されたJSONなのですが、それはさておくとして。

  5. ちなみにts-nodeやwebpackを使う場合は、コンパイル後のJavaScriptファイルを直接使うことはありません。

若手ハイキャリアのスカウト転職