Rのつく財団入り口

ITエンジニア関連の様々な話題を書いているはずのブログです。

【感想】『プログラミングTypeScript――スケールするJavaScriptアプリケーション開発』:日本語で読める最新のTypeScript本で型のある世界へ

(静的な)型が…使える!

 JavaScriptへの注目と共にかのMicrosoft発で2012年10月に発進、2021年4月現在はv4.2.4まで進化したAltJS言語の代表格、TypeScript。2017年にGoogleでも6番目の社内標準言語に採用、フロントエンドフレームワークではAngularJSのv2のAngularが最初から言語にTSを採用、ReactがTypeScriptと親和性が高いのは前から有名、遅れていたVue.jsも2020年秋のv3から対応を強化しました。
 クラウドではコードでAWSインフラを定義するAWS CDKの技術でも、使える言語群に最初からTypeScriptが用意されています。日本でも最近のフロント界隈では規模のある新規開発でJSとTSの2択だったらもう完全上位互換のTSを……という流れになっていますね。
 前から読もうと思っていたので、コロナの中久々に都内に行った時にオライリーのカレンダーと一緒に物理本をGetしていました。原著は2019年、日本語訳は2020年3月発行、著者のBoris Chernyさんは現Facebookのエンジニア。現時点で日本で手に入る最新の、がっつり370ページに及ぶ本格的なTypeScript本です。

www.oreilly.co.jp

1章 イントロダクション

 型安全(type safety)がTSの特徴。型を使てプログラムが不正なことをしないよう防ぐことを、事前のコンパイルレベルの段階で教えてくれるのがTSの特徴だよ……という話を軽妙な語り口調で述べながら、型を求める旅が始まります。

f:id:iwasiman:20210424003120p:plain
型のある世界へ - プログラミングTypeScript

2章 TypeScript:全体像

2.1 コンパイラ

 TSコード→抽象構文木(AST)へ変換されたところで型チェッカー(typechecker)が動いて不正を全てチェック。そのあとJSコードに変換され、ブラウザ内でJSコードが抽象構文木に変換→バイトコードに変換→ランタイムが評価して実行。の流れ。
 CやJavaC#は後半のコンパイルの過程で型チェックが入るのでここが違う。

 本書の語り口だと、読者は通りの向かいの店のバリスタの女の子に一目惚れしている設定になっているらしく、こういうところが面白いです。

2.2 型システム (type system)

 プログラムに型を当てはめるために型チェッカーが使用するルールの集まり。書く人が明示的な構文を使って型をコンパイラーに伝える方法、型を自動的に推論する方法の2つ。TSは両方から発想を得ている。
JSでは型は動的にバインドされエラーが分かるのはほとんど実行時だが、TSでは静的バインドであり、コンパイル時に間違いが分かる。漸進的型付け言語(gradually typed language)なので必ずしもすべての型が分かっていなくても動いてくれる。

 細かいギャグではJavaScriptの魔女バブモーダなる人が出てきます。(ゲームにもなった1980年代のファンタジー映画『ウィロー』に出てくる悪の女王)


www.youtube.com

2.3 コードエディターのセットアップ

 一応他のエディターも上げられてはいますが、作者さんもお気に入りは素晴らしい編集体験をもたらしてくれるVSCodeと書いてあります。まあVSCode一択ですよね。
 言語のオプションはルート直下にあるtsconfigf.jsonで設定。以前はTSLintという静的解析ツールもあったが現在はESLint統一。

f:id:iwasiman:20210424004648p:plain
プログラミングTypeScript

3章 型について

 型(type)とは、型とそれを使ってできる事柄の集まり。できる事柄というのは || とか && とか + とかの演算や操作、メソッド呼び出しも含む。ということでTSのコアである型を述べていく章。

3.1 型についての議論

 function squareOf(n: number)で引数nは数値型という制約を付けるのが、型アノテーション(type annotaion)。注釈をつけるという意味で他の言語でもよく使いますが、TSでは型を指定するために使用。

3.2 型の初歩

  • anyは事実上使っちゃダメといのはまあそうでしょうね。コンパイラでエラーとするよう設定もできるとのこと。
  • unknown型はTSが決して推論することはない型で、特定の型と分かってから初めて演算したりできる。
  • let e:true = true; のようにboolean型の中のtrueしかとりません、とより狭く宣言するのが「リテラル型」(literal type)。このへんが不思議な感じがします。Java使いに自慢できると書いてある……!
  • let f: 12.345 = 12.345; のように特定の数値もリテラル型で宣言できるんですね。ここも初見だと不思議な感じ。
  • あるオブジェクトが中に特定のプロパティを持つかだけを重視して名前は気にしないのが「構造的型付け(structual typing)」=ダックタイピング。JSに習ってTSもこの方針。
    let c: {firstNm: string, lastNm: string}のようにオブジェクトの形状を宣言できる!素晴らしい!
  • キーに対応する値がundefinedでも許すときは、c?: string のように?をつける。
  • [keyName: T]:U で、T型の任意のキー名に対するU型の値がたくさんあることを表現。T型は数値か文字列のみ。
  • オブジェクトの宣言には形状が分かっていれば {key: string}、もしくはobject型。空の{}Object型は避ける。

 こういうことをPureなJavaScriptでやっていた時は関数コメントにオブジェクトの中身を明記したりキー名を分かりやすくしたりでなんとかしていたのですが、おおお……さすが2010年代の言語だ……となります。

  • type Age = number のようにして型エイリアスが可能。これも視覚的に分かりやすそう。
  • 型を | で繋げて合併型(=共用体型、共用型、union型)、&で繋げて交差型。
  • 配列の宣言は Array<string>string[] 両方あるが後者が多い模様。配列の中身の型も推論してくれる!
  • 配列の0番目はnumber固定、1番目はstring固定...などを定義できるのがタプル。readonlyもつけられる。これはオブジェクトで示した方が綺麗であまり使わないような気がします。
  • null ==> 値がないこと
    undefined ==> 値がまだ割り当てられていない変数
    関数の戻り値指定のvoid ==> return文がなく戻り値がない。(あるいは「何もない」が戻る)。ここまではJSと同じ。
    そしてnever ==> 決して戻ることのない関数の戻り値を表す型。必ずErrorが投げられる関数や無限ループの関数など。
  • 列挙型(Enum)でJan, Feb, Mar...など文字列を定義すると実際の値には数値が自動で割当たる。White = '#FFFFFF', Black = '#000000' のように文字列=文字列でも指定可能でこちらのみが推奨。

 数値を使うのは落とし穴があり、本書ではenum自体の使用を控えることを推奨していました。けっこう便利に見えるのですがどうなのでしょう……? やはり定数を持った静的クラスで定義なのでしょうか。

f:id:iwasiman:20210424004648p:plain
プログラミングTypeScript

4章 関数

4.1 関数の宣言と呼び出し

  • 引数は正確には「パラメーター」。useId?: string のように?をつけると省略可能。PHPなどだとこれはnull扱いですがTSではundefinedを表す。
  • useId = 'anonymous' のようにデフォルト引数可能。
  • func(...numbers: number[])のようにして可変長の配列も受け取れるレストパラメーター(rest parameter)がある。これはTSオリジナルの機能ではなくES2015の機能。JavaScriptで非推奨になった悪名高いargumentsは不要で安全! なおRESTful API のレストとは違うので注意。
  • call, apply, bindも使える。この辺自分は使わないので復習します……
  • 悪名高い(?)this.ですが、関数の引数で (this: Date)と何を指すかを指定できる。ありがたい……!
  • ジェネレーターとイテレーターはJSでも自分はあまり使わないので復習してきます……
  • 関数の型はFunction型ではなく、(a: number, b: string) => string のように引数と戻り値を含めた独自の型になる。「呼び出しシグネチャ」と呼ぶ。このへんVSCodeで実際に弄ると分かってきます。記法に=>があるけど関数本体のアロー関数と違うので注意。

  • 予め関数の引数と戻り値をtype FuncNameのように呼び出しシグネチャで定義していれば、関数の実体を記述する時にアノテーションで型は明示しなくても推論してくれる。「文脈的型付け」という。(contextual typing)

  • 呼び出しシグネチャtype Reserve = (from: Date, to: Date, dest: string) => ReserveObj と書いているのは省略形。正式版は{}をつけて
type Reserve = {
  (from: Date, to: Date, dest: string): ReserveObj;
  (from: Date, dest: string): ReserveObj;
}

のように複数指定できる。なんと関数のオーバーロードが可能!(Java世代的に感動しますw)

  • でもJSは引数をチェックしないので、関数の本体で instanceOftypeof でチェックしないといけない。

 全体的にJavaScriptもほんとはこう動いてほしいんだけどな……とか、なんかJSのこれって地雷が埋まってそうだから避けよう、としてきたところがTypeScriptだと言語仕様でうまく吸収され、安全に使えるようになっている印象です。

4.2 ポリモーフィズム

  • 型レベルの制約を強制するために使われるプレースホルダーを「ジェネリック型パラメーター」(あるいは多層型パラメータ―)と呼ぶ。
  • 下のように定義する。第1引数がT型の配列。第2引数がフィルタする関数で引数がT型、戻り値が残るか弾かれるかのboolean。外側の関数の戻り値がフィルタされたT型の配列、という意味。
type Filter = {
  <T>(array T[], f: (item: T) => boolean): T[];
}
  • 型を表す変数名はオブジェクト指向よりなら慣例的に T, U, V... を使う。
  • これによって型シグネチャで概略を記述、その後で値を埋め込んでいくプログラミングスタイル「型駆動開発(type-driven development)」が可能になる。シグネチャを見れば大体関数の中身が想像がつくようになる。

 最終的にはJavaScriptになって動く訳で独特なところもあるのですが、Javaにおける ArrayList<String> みたいなことも普通に書くことができ、おおお……TypeScriptすごいな!となりました。

f:id:iwasiman:20210424004648p:plain
プログラミングTypeScript

5章 クラスとインターフェース

 最初はチェスのクラスを例にとり、TypeScriptの大きな特徴であるクラス周りを解説しています。

  • 他のオブジェクト指向言語と同様にクラス宣言ができる! extendsで普通に継承できる。
  • クラスのプロパティ(=メンバ変数)、メソッドにはアクセス修飾子でpublic, protected, private
  • abstract class { ... と宣言すると抽象クラスになりインスタンス生成不可。しかしメソッドは定義できる。
  • 親クラスのメソッド呼び出しは super.parentMethod() 、コンストラクタ呼び出しはsuper()
  • メソッドの戻り値にthisを使うと自身のインスタンスを表せる。

親クラスを呼び出すのはC#寄りの base() でなくJava寄りの super() なんですね。

  • type Sushi = {} と似た記法で interface Shushi {} が定義できる。
  • 違い1. 型エイリアスは右辺に A | number のように任意の型を指定できるが、インターフェースはこれができない。
  • 違い2. interface B extends A と継承すると拡張元のメソッドの型をチェックする。型エイリアスの交差型は違う動きをする。
  • 違い3. 同じスコープに同名の型エイリアスがあるとコンパイルエラー。同名のインターフェースがあると「宣言のマージ」をしてくれてひとつになる。

  • インターフェースはメソッドだけでなくインスタンスプロパティも宣言できる。アクセス修飾子や staticは だめ、readonly は指定できる。

  • インターフェースは形状(Shape)をモデル化するための方法で汎用、軽量。コンパイル時まで存在してJavaScriptコードにはならない。このクラスはAnimalである、などを表現する軽量な方法。

  • 抽象クラスはクラスをモデル化、コンパイル後もJavaScriptコードになる。機能豊富。複数のクラスで実装を共有する時に使う。(親クラスで処理を共通化とか)

インターフェイスがマージされるとか、JSコードにならない話はやはりTS独特だな……と思います。

  • TypeScriptではクラスは名前でなく構造によって比較、区別される。下の関数では引数のクラスがwalk()メソッドを持ってさえいれば何のクラスかは気にしない。なおフィールドのアクセス修飾子はチェックされる。
function takeAWalk(animal: Zebra) {
  animal.walk();
  animal.orginName = ''; // privateになっていたらこれはだめ
}
  • let a = 123; は値を宣言、type a = number; は型を宣言。TSでは型と値の名前空間が別々。クラスと列挙型はこの両方を生成する。
  • class MyMap<K, V> のようにジェネリック型をサポート。普通にポリモーフィズムが表現できる。
  • mixinの構文や機能はないが、実装で実現可能。
  • 実験的なデコレーターの機能があるが将来は不明。ECMAScript仕様でも未定のため。
  • finalキーワードはないが、コンストラクタをprivateにすると拡張を禁止にできる。このケースでは static create() などでインスタンスを返すメソッドを追加すればよい。
  • これらの機能を使ってデザインパターンも実現できる。

 当方オブジェクト指向デザパタ原理主義者ではありませんが、このへんのことも書けるのを見ると静かに感動します。そしてファクトリーパターンの例がちょっと分かりませんでした。練習問題の方は分かるのですが本文のコード、new クラス名 の後になぜ()がないのだろう……?

f:id:iwasiman:20210424004648p:plain
プログラミングTypeScript

6章 高度な型

Type周りのさらに高度な話の章。

6.1 型の間の関係

  • A, Bの型があり、Aのサブタイプ(派生型,subtype)がBなら、Aが要求されるところではBを安全に使える。
  • A, Bの型があり、Bのスーパータイプ(上位型,supertype)がAなら、Bが要求されるところではAを安全に使える。
  • B <: A がBはAのサブタイプか同じ型。
  • A >: B がAはBのスーパータイプか同じ型。
  • 4種類の変性が不変性、共変性、反変性、双変性でよく使うのが共変性、<:T
  • クラスの場合はまあ分かるが、関数が難しい。関数AのサブタイプがBであると言える場合、this型とパラメーター(引数)はA>:B、戻り値の型はA<:Bが条件。
  • const c = true;true型、let c = true;boolean型なのは、TSが型を拡大してくれるから。let c: true = true; と明示的に型を示すとtrue型となり拡大を防げる。
  • let c = {x: 1} as const; のようにas constを付けると、型の拡大を防ぎかつreadonlyにしてくれる。
  • type Options = {a: string, b:string ....} のように定義した型のプロパティ名をタイプミスしてもTSが教えてくれるのは、過剰プロパティチェックという機能が自動で動くから。

6.2 完全性

  • type Weekday = 'mon'|'tue'| ....のようにしてこれを関数の戻り値にすると、分岐が足りない場合は警告してくれる。tsconfig.json"noImplicitReturns": trueにする。

6.3 高度なオブジェクト型

  • Web APIの戻り値が複雑な構造のJSオブジェクトの場合、type FriendListNumber = APIResponse['user']['friendList']['count']; のようにJSの書き方と似た形式で型を認識できる「ルックアップ」がある。
  • type Keys = keyof APIResponse; // 'user'|'hoge' のようにkeyof演算子を使うと、オブジェクトの全てのキー一覧が入ったunion型を作れる。
  • let day: Record<WeekDay, day> = {}; のようにunion型を指定すると制約を引き継ぐことができるRecord型。
  • もっと便利なのがMap型で独自構文がある。予めよく使うものがTS自体に組み込まれている。Partial<T>, Required<T>, Readonly<T>, Pick<T,Keys>など。
  • 同じ名前のオブジェクトとクラスを両方使える「コンパニオンオブジェクトパターン」

 このへんになるとぐんと難しくなってきます……! けっこう数学の領域に踏み込んできます。実際にコードを書いていると感覚的に分かってきそうな気もしますが、やっぱり高度な使い方は難しいなと。

  • type IsStringType<T> = T extends string ? true : false; のように三項演算子的に型が分岐できる「条件型」
  • 条件の一部としてジェネリック型を宣言できるinfer
  • あまり使わない方が良い、高度な逃避手段(エスケープパッチ)
    • input as string のようにこの変数はここに来るまでに必ず文字列が入ることを主張する型アサーション
    • id!のように感嘆符を付けるとnullでないことを主張できる非nullアサーション
    • 変数にundefinedでなく既に値が入っていることを主張できる割り当てアサーションも、変数名の後に!を付ける
  • 例えばID文字列のように入る値にルールがある変数に値を強制できる型のブランド化のテクニックがある
  • JavaScriptの言語自体のプロトタイプ拡張も、TypeScriptならより安全にできる

 まとめにすべてのものを理解できていなかったり覚えられなかったりでも大丈夫、とあって安心しました。このへんはかなりTSを使い込まないと実感が湧いてこなさそうです。

f:id:iwasiman:20210424004648p:plain

7章 エラー処理

 愉快なプログラマー小噺を冒頭に挟んで、TSのエラー処理パターンを論じる種。ユーザに誕生日の入力を促してそれを日付型にするfunction parse(birdtday: string)関数を例にとって解説しています。

1==>
parse(birthday: string): Date | null にして、中で異常が発生したらnullを返す。軽量な方法だが、呼び出し側にエラーの情報が渡らない。

2==>
parse(birthday: string): Date にして、中で異常が発生したら throw new ThisError('エラー原因')する。
 呼び出し側は try {....} catch (e) {....} で囲み、if (e instanceof ThisError)で場合分けして処理。予め何種類かErrorクラスがあるので拡張したクラスを作っておく。外側に情報が渡る。
関数のJSDocには@throwsで書いておいた方がよい。怠惰なエンジニアはtry-catchを忘れる可能性がある。

3==>
parse(birthday: string): Date | ThisError | ThatError と戻り値を列挙型にして、中で異常が発生したらthrowでなくreturn new ThisError('エラー原因') と戻り値で返す。Javathrowsと似た形。
 呼び出し側は if (result instanceof SomeError) で場合分けして処理。コーディング時に処理が足りないとTSがTypeErrorを出してくれるので、TSの強みを活かせる。

4==>
自前で Option<T>, Some<T> を実装。flatMapgetOrElseメソッドを作り、失敗する可能性なる一連の操作をうまく扱える。

 最後のOptions型というのが高度で理解しきれませんでした……あまり使っているところを聞きませんが、日本のTSの現場ではどうなのでしょうね。エラー処理の方法は完全にあなた次第とあり、やはり3番目の例外を戻り値で返す方法かなあと思います。(try-catchネストも減るし。)
 最後のOptionの話でHaskell言語より優秀だぜ!(ドヤ!)と本文ではTSをとことんアゲつつ、訳注でそうでもないですと冷静に書いてあったりして(笑)、このへんは公平でありがたいです。

f:id:iwasiman:20210424004648p:plain
プログラミングTypeScript

8章 非同期プログラミングと並行、並列処理

8.1 JavaScriptのイベントループ

 setTimeout()の例を載せ、JavaScriptの内部的な動きを解説しています。

8.2 コールバックの処理

 node.jsを使った非同期プログラミングでは、関数呼び出しの引数にコールバック関数を使う。しかしネストして複雑になっていくと途端に難しくなる話。本書では「コールバック地獄」でなく「コールバックピラミッド」というワードを使っています。

8.3 プロミスを使って健全さを取り戻す

 これを解決する方法がお馴染みPromise。この節では練習問題として、型指定を活用しながらPromiseクラスを自作する方法が書いてあります。

8.4 asyncとawait

 外側の関数をasync function getFoo(), その中の各処理をawait getBar() ....と書いていくと、try-catch-finallyで上から同期的に順番に実行するように書ける話。TypeScriptでもこの構文を完全にフォロー。

8.5 非同期ストリーム

 さまざまな型のデータが非同期にどんどん溜まっていく場合に使うデザインパターンであり実際のライブラリもあるEventEmitter周り。TypeScriptではこれらを作る際に型を指定して安全に定義できる話。

8.6 型安全なマルチスレッディング

JavaScriptでは1つのスレッド内を分割してうまく処理しますがスレッドが複数になる本物の並列処理の話。

  • ブラウザ上でマルチスレッドのやり取りをするWeb Workerのメッセージパッシングでも型を活用する
  • npmで @types/node をインストール、Node.jsの子プロセスを使う際もTSで型を使う

 この章も難しく感じました……!(ぐぬぬ) JavaScript自体の非同期周りを復習しないとなあ。

f:id:iwasiman:20210424004648p:plain
プログラミングTypeScript

9章 フロントエンドとバックエンドのフレームワーク

 お待ちかねフレームワーク群とTypeScriptの章。本書はTS自体の本なので、フレームワークについてはあまり多くは語られていません。

9.1 フロントエンドのフレームワーク

  • すべてのJavaScript組み込みDOM APIが型安全で使え、コーディング中に警告してくれる。tsconfig.jsonを以下のように設定しておく。
{
  "compileOptions": {
    "lib": ["dom", "es2015"]
  } 
}
  • ReactにTSを使うのは素晴らしい選択。tsconfig.jsonに以下のように設定。esModuleInteroptrueだと、コンポ―ネント記述冒頭のインポート文はimport * as React from 'react'; でなくimport React from 'react'; になる。*.jsxでなく*.tsxファイルに書く。
{
  "compileOptions": {
    "esModuleInterop": true,
    "jsx": "react"
  } 
}
  • 関数型コンポーネントコンポーネント自体の引数(props)を(props: Props) として別途type Propを定義したり型を指定したり。TypeScriptのコンパイラがJSXの形式やコンポーネントに渡すプロパティの過不足をチェックし、強制してくれる。コンポーネントの宣言でconst app: React.FunctionComponent<Props> のように書く最近のやり方は本書では記述なし。
  • 従来のクラスコンポーネントprops, stateを別途型を指定して書いたり。stateの中に存在するかのチェックや、this.stateに直接書き込んでいないかなどもコンパイラが弾いてくれる。React自体のPropTypes機能はいらなくなる。

  • Angular6,7。Angular自体がTSで書かれている。

  • Angularのライフサイクルフックとして用意されている関数群(OnInitなど)がTSのインターフェースになっている。
  • クラスやオブジェクトの前に@つきでメタデータを記述する「デコレーター」はTypeScriptの実験的な機能だが、Angularはこれを多用している。サービスで依存性注入(DI)を可能にしている部分でもデコレーターを活用。

9.2 型安全なAPI

  • 通信でJSONを渡すところなどを、TypeScriptのunion型で工夫することもできる。
  • API仕様から型が付けられたコードを生成するツールあり。SwaggerCodegen, GraphQLAppoloRelayRPC用のgRPCApache Thrift

9.3 バックエンドのフレームワーク

  • バックエンド(サーバーサイド)をJS/TSで書く際、DBアクセスにもnpmにライブラリがある。node-postgresnode-mongodb-nativeなど。生でSQLを書くが、TSならこれらの戻り値に型が書ける。
  • TypeScriptのORMツールではTypeORMが有名。ほとんどのRDBMongoDBに対応。型安全な高レベルAPIになっている。

typeorm.io

 日本語版で2020年刊行の本なのでReactについてはクラスコンポーネントよりまず先に関数型コンポーネントを紹介、useStateをちゃんと使っていました。
 AngularはあるのにVue.jsがないのは、TypeScriptとあまり親和性が高くないからでしょうか。Vue.js3でTypeScript対応が強化されたのは2020年秋なので、さすがに本書には間に合っていませんね。

f:id:iwasiman:20210424004648p:plain
プログラミングTypeScript

10章 名前空間とモジュール

10.1 JavaScriptモジュールの簡単な歴史

  • 1995年リリース時のJavaScriptはモジュールシステム一切なし、すべてがグローバル名前空間でデンジャラス。
  • グローバルなwindowオブジェクトにwindow.someModule = ....のように割り当てる即時実行関数式で工夫したりしていた。
  • その後2004年にDojo、2005年にYUI、2009にLABjsがモジュールローダーの仕組みを考案。
  • 2009年のNode.jsはこれらの教訓からモジュールシステムを組み込むことを決定。CommonJSというモジュール標準を使用。
  • DojoRequireJSが推進したAMDというモジュールもあった。
  • 2011年にBrowserifyが登場、CommomnJSをフロントエンドで使えるようになった。しかし呼び出しが常に同期的、静的解析できない場合があるなど欠点あり。
  • ES2015(ES6)ではインポートとエクスポートの標準を導入。これが現在の標準。TypeScriptでも使っている。他の方法も用意されている。

 こういう歴史が書いてあるのはありがたいです。2000年代というと僕はサーバーサイドJavaを勉強してバシバシ使ってましたが、確かにDojoとか一時期あったなぁ……

10.2 インポート、エクスポート

  • TypeScriptでもJS標準の記法を使う。
// a.ts 内
export function func1() {}
export function func2() {}
// b.ts 内
import {func1, func2} from './a'
func1()

// c.ts デフォルトエクスポート
export default function def_func() {}
// d.ts デフォルトエクスポートからの場合はimportの後の{}がいらない。
import def_func from './c'
def_func()

// e.ts ワイルドカードで全てインポート
import * as a from './a'
a.func1()
a.func2()
export * from './a' // 再エクスポート
  • TSでは値と型が別の名前空間に存在するので、同じひとつの変数に入れても別扱いで推論してくれる。
// g.ts
export let X = 1
export type X = {message: string}
// h.ts
import {X} from './g'
let aaa = X + 1 // 値Xが使われる
let bbb: X = {message: 'Hello'} // 型Xが使われる
  • let locale = await import('locale_ja') のようにして遅延読み込みができる。tsconfig.jsonを変更。
  • tsconfig.jsonを変えてCommonJSAMD標準のモジュールも一応呼べる。
  • 基本的にモジュールモードが動くが、一応スクリプトモードというものもある。

10.3 名前空間

naemspace Ns1 {
  export namespace Sub1 {
    export function getFoo(): string {
    }
  }
  export namespace Sub2 {
    export function getBar(): string {
    }
  }
}
// 同一の名前空間が複数ファイルにまたがっていても、コンパイル時にマージしてくれる。(!)
// importした別のtsファイル
Ns1.Sub1.getFoo()
// 名前空間エイリアスという別名も使える。
import d = Ns1.Sub2.getBar
d()
  • 名前空間が競合するとコンパイルエラーになる。
  • 明示的な依存関係があると読みやすく、モジュール分離を強制させ、静的解析が楽になる。複数ファイル分割はパフォーマンスでも有効。中規模以上のプロジェクトでは名前空間よりもモジュールを使うことを推奨。

10.4 宣言のマージ

 同名の値と型、同名の名前空間、同名のインターフェイスがマージされる話を表で一覧化してくれています。

 Udemyの講座でTypeScriptを実際に触った際、名前空間の中にクラスを入れて書くとインデントが1つ増えてあまり嬉しくないし、この記法はC#ぽいなあと思いました。
 本書では明確に名前空間よりモジュールで分ける方を勧めています。JSのお作法的にもやっぱりそうなんですね。

f:id:iwasiman:20210424004648p:plain
プログラミングTypeScript

11章 JavaScriptとの相互運用

 既にJavaScriptのコードベースを使っている方には嬉しい、実践的なTypeScriptへの移行の章。

11.1 型宣言

  • *.tsに対応した *.d.ts が型宣言dファイル。declareキーワードを使って型宣言だけを行っている。
  • 事前に明示的にインポートせずともグローバル変数を使えるようにするのをアンビエント変数宣言という。例えばNode.jsに出てくる変数processだったらpolyfill.tsの中で定義されている。
  • 同じく型を宣言するのをアンビエント型宣言という。スクリプトモードの .ts, .d.tsの中で宣言。
  • 同様にアンビエントモジュール宣言というのもある。

11.2 JavaScriptからTypeScriptへの漸進的な移行

  • 1::: tsconfig.jsoncompilerOptions"allowJs": trueにする。コンパイラTSCがJSも一緒にコンパイルしてくれるようになる。
  • 2-a::: "checkJs": true にするとTSがJavaScriptコードについても能な限り型推論を試みてくれる。大体はanyになる。ファイルの先頭に // @ts-nocheck, // @ts-check を付けておくと判別してくれる。
  • 2-b::: JSコードの関数の上の行にJSDocをちゃんと書いておくと、これも型推論に使ってくれる。
  • 3::: *.jsファイルを*.tsファイルに変えていく。エラーを正しく潰しながら1ファイルづつ進む方法、最初はコンパイラーを緩くして全体を移行してから対応する2つの方法がある。
  • 4::: 最終的には"allowJs": false, "checkJs": false にして厳格にする。TSだけの世界に移行完了!

11.3 JavaScriptの型の探索

  • 同一プロジェクトのJSコードでは、.jsファイルと同じディレクトリにある.d.tsを探しに行く。
  • npmから入れたライブラリは、まずpackage.jsonの中を見て、次に node_modules/@types/{ライブラリ名} を探しに行く。後述のDefnitelyTypedプロジェクトで型を定義してくれている。

11.4 サードパーティーJavaScriptの使用

  • 有名なライブラリは中に型宣言を備えていて、compilerOptions"noImplicitAny": true でもそのまま使える。
  • そうでない場合はOSSDefnitelyTypedで誰かが定義してくれている。@typesスコープの下で公開されているので、> nom install @types/react のようにしてインストールすると使える。
  • DefnitelyTyped にも定義されていなかったら、自力でやってコントリビュートするとよい。

 Udemyの講座でTypeScriptを弄っていると謎の.d.tsファイルが確かに出現してくるのですが、なるほど型宣言のファイルだったのですね。@types/react とか何だろうと思っていたのですが様々なライブラリの型宣言自体が別のOSSになっていたのか……!
 既存のJavaScriptエコシステムとうまく調和するように作られているのだなあと思いました。
 小ネタとしては映画『ウィロー』からJavaScriptの魔女バブモーダのネタがまた出てきます。(弟子の名前、ググっても出てこない……)

f:id:iwasiman:20210424004648p:plain
プログラミングTypeScript

12章 TypeScriptのビルドと実行

 最後はビルド周りの章。

12.1 TypeScriptプロジェクトのビルド

  • src/配下にフォルダ構造を作って*.tsファイルをコーディング、dist/配下にフォルダ構造そのままにコンパイル後の*.js*.d.tsを置くのがお作法。なるべくこれに従う。
  • コンパイラの設定を変えるとJSファイルの*.js.mapのソースマップ、*.d.tsの型宣言、そしてコンパイル時間短縮の宣言マップである*.d.ts.mapも出力できる。ソースマップがあるとChrome開発者ツールなどが認識してくれる。
  • バックエンドでNode.jsでJSを実行する際は、JSバージョンは自由。
  • バックエンド用のOSSライブラリとしてリリースする場合は、対応するNode.jsのバージョンを書くぐらい。
  • ブラウザ上で動くJSの場合が厄介。ポリフィルを使ったり、古いブラウザにメッセージを出したり。
  • ブラウザ上でもバックエンドでも両方動くアイソモーフィック(isomorphic)なライブラリは、Node.jsの最低バージョンを保証し、かつブラウザの様々なバージョンに対応できるよう広い対応が必要。
  • JSのバージョンは、compilerOptions"target"を指定。es3, es5, es6かes2015, es2016, es2017, es2018, esnext。迷ったらes5
  • JSの新しい機能を振るいバージョンにポリフィルする必要がある時は、compilerOptionslibに文字列配列で指定。ブラウザ上で動かす場合は"dom"も指定。
  • TSを幾つものプロジェクトに分ける場合はプロジェクトごとにtsconfig.jsonを作り、オプションに書く。

12.2 TypeScriptをサーバー上で実行する

  • compilerOptons"module": "commonjs"にし、"target": "es2015"にする。importexportが使えるようになる。

12.3 TypeScriptをブラウザー内で実行する

  • モジュールバンドラーごとにガイドあり。TypeScriptのコンパイラTSCは高度な機能はないので、TS用プラグインを使うとよい。Webpackにはts-loaderがある。

12.4 TypeScriptコードをnpmに公開する

  • 他の人が型を使えるよう型宣言を生成したり考慮が必要。npmにはコンパイル後のjsファイルだけ上がり、git上はコンパイル前のtsファイルだけを上げるよう、.npmignore, .gitignoreファイルを修正するとよい。

 やはり細かな設定はいろいろありますね。なるべくcreate-react-appとかNext.jsなどのデフォルトのお作法に従った方がいいのだろうなあと推測しました。

f:id:iwasiman:20210424004648p:plain
プログラミングTypeScript

13章 終わりに

 最後のまとめの後は40ページにもわたる圧巻の付録が付いています。型の話やオプション一覧など、実際に使う時のお役立ち情報が満載です。
 そしてお得なのが日本語版オリジナル、翻訳の今村謙士さんによる「付録H ESLintとAST」。TSLintが2019年に非推奨となった後、JSと同じようにTSでもデフォルトになったESLintの使い方、独自ルールの作り方が、かなり細かいレベルで解説されています。これもありがたいですね。

www.typescriptlang.org

f:id:iwasiman:20210424004648p:plain
このフォントはHelvetica Neueですが、TS公式ロゴはWIndows 10 標準のSegoe UIとのこと

まとめ:満を持して登場の、日本語で読める最新のTSの書

 基礎から応用まで、さすがにオライリーの本はしっかり一通り書かれているな……という印象です。元からユーモア交じりの砕けた文章なのですが文章は読みやすく、日本語訳も自然でそれほど引っかかるところはありません。あまり日本語で読めるまとまった本がない中で、最新の一式揃った本格本として、1冊手元に置いておくと学びになると思います。注釈の中には日本語版独自の訳注もあってありがたいです。

 欠点は特にないのですが、あえて言うと時々出没するJavaScriptの魔女バブモーダを始め、日本人からすると言い回しが分かりにかったりする英語特有の謎のジョークがところどころ出てきます。(ムッソリーニって確かに悪い人だけど技術書の中で冗談に使うもんなの?と思ったりしました。)
 作者のBoris ChernyさんはかのFacebookでご活躍中のエンジニアでTypeScriptが大好きなんだなというのはよく分かったのですが、「Javaを使っている友人に自慢しましょう!」「Haskellの(ry」みたいな、日本で下手にネットでイキッて吹聴したら叩かれそうな表現も所々出てきます。まあこれは言語を推進する立場からのポジショントークというか、アメリカの一流エンジニアでもこういうもんなんだなぁと思うことにしました……(笑)

 TypeScript言語自体の方はまだ業務で本格導入はしていないのでUdemyの講座をやったりしているのですが、ドットを打った時の補完とか、間違うとすばやく出てくるエラーとか修正候補の表示とか、実際にVSCodeでコードを書くとそのありがたみが分かりますね。
 僕自身はJavaScriptがまだいらない子だった時代からWebサイトでいじったり、仕事の方はメインはJavaC#など型の制約の強い世界でキャリアの基礎を固めてきました。そういう視点からすると、おお……ちゃんとクラスも継承も使えてデザインパターンとか使えるじゃん……と思ったり、フリーダムなJavaScriptもほんとはこういう動き方をするようにちゃんと厳格に育ってほしかったんだよ!という点がTypeScriptだと実現されていたり、謎の感動を味わいました。
 それでいてエラーメッセージで賢く助けてくれるのはコンパイルされるまでで、その後は動く実物の中では動的なJavaScriptに戻って実行される。厳格さと柔軟さを兼ね備えた新時代の言語。
 いやはや面白いこと考えたものです。新しい考え方というのはどんどん出てくるのだなと改めて感じました。

f:id:iwasiman:20210424003120p:plain
型のある世界へ - プログラミングTypeScript

リンク集

オライリーのページ。かなりはてブがついて注目されました。 www.oreilly.co.jp

本書のGitHubリポジトリ。正誤表に飛べます。 github.com

作者のBoris CherryさんのブログとGitHubhttps://performancejs.com/performancejs.com github.com

ネットの感想では以下のあたりが見つかります。

日本語で読めるTypeScriptの本

Kindleのみですが『速習 TypeScript 第2版 速習シリーズ』が2020年3月。僕も読みましたがこの本は薄く、速く読めて概要を掴むにはお勧めです。

iwasiman.hatenablog.com

ディー・エヌ・エーの吉井 健文さんによる『実践TypeScript ~BFFとNext.js&Nuxt.jsの型定義~』が2019年。この本もよく聞きます。

他、翻訳本の『TypeScript実践プログラミング (Programmer's SELECTION)』が2015年、『JavaScriptプログラマのための 実践的TypeScript入門』が2014年。他にも見つかりますが、このへんになるともう情報が古いので非推奨ですかね。

TypeScript, Reactをはじめとするフロントエンドフレームワークの入門本まとめ記事最新版が、このブログ内のこちらにあります。

iwasiman.hatenablog.com