TypeScript入門以前ガイド
某社で自分が React/Redux + TypeScript などの講習をやってみた結果、TypeScript 入門用資料が必要だと思って書いたやつです。
このドキュメントのターゲット
- TypeScript で書かれたプロジェクトに参加する人
- TypeScript を導入するために、その事前知識が必要な人
自分が React/Redux などの講習でいろいろやってみた結果、 ES2015 と TypeScript を同時に教えると混乱すると、初学者は圧倒されて混乱します。なので、ES5 -> ES2015, ES2015 -> TypeScript にわけて解説することにします。
先に言っておくと、 TypeScript 固有の機能というのはほぼ存在せず(ほぼ enum のみ)、ES2015 の型アノテーションの文法がちょっとずつ拡張されているだけです。
このドキュメントの読み方
前提: プログラミングの基本がわかること(四則演算、変数、関数などの概念)
- JavaScript を詳しく知らない => ES2015 for Beginners 編へ
- ES5 + jQuery の人 => ES2015 for ES5 Programmers 編へ
- ES2015 はわかるが、 import/export がわからない => ES Modules 編へ
- ES2015 + ES Modules がわかる => TypeScript 編へ
- 実運用を知りたい人 => エコシステム編へ
最初に ES2015 の解説をして、次に TypeScript がそれをどう拡張するか説明します。
ES2015 for Beginners
あなたはプログラマーとして JavaScript を初めて学ぶ人間です。プログラミングの基本的な概念は知っています。もしかしたら JavaScript を少しいじったことがあるかもしれませんが、大きなアプリケーションを書いたことはありません。
この言語は複雑怪奇な(残念な)歴史を持っており、いろいろな都合で謎の慣習があったりなかったりしますが、新しく学ぶ人は ES2015 と名乗る仕様だけを追えばオッケーです。むしろ、2015 年より古い情報は、目を通さない方が良いぐらいです。
今の JavaScript は、ES2015, ES2016, ES2017... と毎年言語の仕様が更新されます。ES2015 は特別なバージョンで、このリリースサイクルになる基点で、かつ大規模な機能追加が行われたバージョンだからです。ES とは EcmaScript の略で、ブラウザによる拡張などを除いた、純粋な言語機能だけを指します。
IE 以外のブラウザ(Chrome, Safari, Edge, Firefox)は、いずれも 2 ヶ月から 6 ヶ月の間隔で更新され、どのブラウザも ES2015 と呼ばれる水準なら、基本的にカバーしています。
しかし、現実には 2018 年においてはまだ IE をサポートしたい、するべきという意見が多数派でしょう。
そのため、JavaScript の開発者コミュニティにおいては、ES2015 で書いたコードを ES5 に変換して配布するのが主流です。IE のサポートが切れる 2021~2024 年まで、この状況は続くことでしょう。
この記事ですべての文法を解説しませんが、日本語でのES2015の入門は、 JavaScript の入門書 #jsprimer を推奨しています。
知っておくとよい雰囲気
- 基本的な文法自体は,比較的シンプルな C/Java の系譜
- 関数が number や object のようにファーストクラスオブジェクトであり、つまり変数に代入可能。関数参照なので、オーバーロード不可
- 歴史的経緯によって Java から多数の予約語を借用しているが、Java との互換性はない
- 歴史的経緯と後方互換性によって、非推奨な機能が多々残っている。具体的には
with(...) {...}
,var
による変数宣言,==
による比較など。 - 関数型を目指した時期(ブレンダン・アイクによる初期実装)、Java を目指した時期(廃止された ES4 と ActionScript との分岐)、Python に影響を受けた時期(Mozilla による JavaScript 1.x)、CoffeeScript 経由で Ruby に影響を受けた時期(Arrow Functon, class), TypeScript 経由で C# に影響を受けている時期(最近)がある
- 使う人によって、書かれるコードの雰囲気がかなり異なる
ES2015 for ES5 Programmers
あなたは一昔前の JavaScript で生きてきた人間です。 jQuery の API は一通り知っています。サーバーで生成した HTML にクラスをつけて $(...)
でセレクタを捕まえて、その中身を書き換えてきました。$.map(...)
による制御や、 var self = this
のようにクロージャの this を保存して再利用してきたかもしれません。
ES2015 入門するにあたっては、ES5 までに覚えた「お約束」は、全部忘れてください。
var
による変数宣言は推奨されなくなりました。再代入しない値はconst
、再代入を許可する変数はlet
を使ってください。ほとんどconst
でいいはずです。 const - JavaScript | MDNwindow.App = {...}
のようなグローバル変数によるモジュール渡しは行いません。トップレベルスコープでの代入も暗黙のグローバル変数への推奨されません(onload = function() {...}
)。ファイルスコープと、import/export
による参照渡しによって行います(ES Modules 編を参照)- jQuery の DOM 操作は、昔はブラウザ間の差異を吸収する役割がありましたが、今では標準 API で十分で、そちらが推奨されています。たとえば
$(".js-items")
ではなく、document.querySelectorAll(".js-items")
を使ってください。 You Might Not Need jQuery var self = this
は、 arrow function によってほぼ不要になりました。これは、function() {...}
を() => {...}
と書ける機能なのですが、この関数スコープ内のthis
は親スコープの this を引き継ぎます。 アロー関数 - JavaScript | MDN- ES2015 の ES Modules 環境においては、トップレベルの
this
はundefined
です。window
ではありません。 - 非同期表現は
jQuery.Deferred
ではなくPromise
を使ってください。Promise による API が利用可能ならば async/await を使ってください。jQuery3 以降の jQuery.Deferred は Promise と互換になりましたが、レガシー環境では jQuery 2 以上の採用は稀です。 Promise を使う - JavaScript | MDN - 複雑な prototype 継承は(表向きには)行わなくました。
class
宣言 を使ってください。内部的には prototype 継承イディオムと同じことが行われています。 クラス - JavaScript | MDN - ネットワーク通信は
jQuery.ajax
ではなく、window.fetch
が推奨されます。ただし、fetch 自体が冗長な使い方を必要になるケースが多く、post を多用する場合は axios の方がよく使われます。いずれも Promise ベースの API です。 Fetch 概説 - Web API インターフェイス | MDN axios/axios: Promise based HTTP client for the browser and node.js
ES Modules
ES Modules(略称 ESM) は JavaScript に他の言語のようなモジュールシステムを導入する機能です。
import/export によって、外部のファイルパスを指定することで、export されたオブジェクトを呼ぶことが出来ます。また、ファイルごとにファイルスコープを持ちます。(僕が知ってる言語の中では python の module system が一番似てます)
// src/a.js
import b, { c } from './b.js'
b();
console.log(c);// src/b.js
const v = 1; // 外から見えない// default は特殊化されています
export default () => {...};
export const c = v;
依存が静的に決定されるので、グローバル変数同士で依存がある場合と違って、初期化順の問題が発生しません。(循環参照だと未解決のundefined
になることはある)
IE 以外のモダンブラウザでは、<script type="module" src="main.js"></script>
という風に呼ぶと、 ESM でコードを書くことが出来ます。
ただし、機能として実装されているだけで、実際にはあるファイルをエントリーにした時、「非同期なファイル取得 => パース => 依存決定 => 非同期なファイル取得 => パース => ...」という処理を繰り返してしまうので、お世辞にも最適化されてるとは言えません。依存を静的に解析して配信サーバーを最適化するための仕様がまだ安定していないためです。(将来的にHTTP/2 Push をベースに読み込みを最適化する仕組みが考えられています)
IE で動かないこと、読み込みの最適化が行われないこと + npm のモジュールを依存に含みたいことが多い、といった理由で、Webpack というツールで、一つのファイルをエントリーにして、一つのファイルに固めてしまうことが現状ベストプラクティスとされています。
おまけ: CommonJS について
2011~ に書かれたコードと、NodeJS のコードは module.exports = ...
と require('...')
による ESM ではないモジュールシステムを見ることがあります。これらはファイルシステムの相対パスでの参照解決という点では ESM と似ていて、実際に ESM の仕様策定に大いに影響があったのですが、これを browserify
というツールでクライアントでも node.js のエミュレーションをする、というツールが流行った結果、これをモジュールシステムとして採用するフレームワークが増えました。
node.js では未だに ESM の扱いが experimental なので、「モジュールシステム以外は ES2015」というライブラリも比較的よく見かけます。
今盛んに使われている webpack も初期バージョンでは ESM を一旦 commonjs に変換して解決していましたが、今のバージョンでは import を直接解釈して結合しています。
Webpack によって、ESM と commonjs は相互読み出しが可能になっているのですが、 commonjs では default に相当する概念がないので、扱いがやや特殊になっています。commonjs から export default {}
を参照したい場合 require('./foo').default
という形になります。ただし、Webpack による特殊化であって、標準化された振る舞いではありません。
非同期表現: Promise と async/await
(やや難しいので必要になるまで読まなくて良い)
Promise
は JavaScript で非同期処理を表現するための機能です。
「1 秒待つ関数」を表現するのは次のようになります。
const wait = () =>
new Promise((resolve, reject) => {
setTimeout(() => resolve(), 1000);
});wait().then(() => console.log("wait done"));
reject(...)
を呼ぶと失敗処理になります
const willFail = () =>
new Promise((resolve, reject) => {
reject(new Error("reason..."));
});willFail()
.then(() => console.log("never"))
.catch(() => console.log("come here"));
ES2017 では、これを構文的に表現する async/await という文法が追加されました。
const main = async () => {
await wait();
console.log("after 1000");
try {
await willFail();
} catch (e) {
console.log("come here");
}
};main();
内部的には Promise(と正確には ES2015 で追加された ジェネレーター関数) の糖衣構文です。
async で宣言された関数の中では、 await で Promise の解決を待ちます。 await の中での非同期例外は 例外機構 try {...} catch(error) {...}
で表現されます。 また、この関数の返り値は必ず Promise のインスタンスになります。関数が終了すると resolve される Promise オブジェクトです。
トップレベルスコープでの await は今現在、仕様で許可されていませんが、将来的には可能になる予定です。Chrome の DevTools では例外的に許可されています。
基本的には、非同期関数は Promise で表現しつつ、使う側は async/await で解決する、という形になるでしょう。
TypeScript
TypeScript の型は、基本的には Java や C# と似ています。Javaの影響を受けた C# の作者 Anders Hejlsberg が設計した JavaScript 拡張です。
入門資料
TypeScript in 5 minutes · TypeScript
typescript-ninja/typescript-in-definitelyland: TypeScript in Definitelyland ちょっと古い
バージョンごとの変更点は vvakame さんの Qiita が参考になります 「user:vvakame tag:typescript」の検索結果 - Qiita
TypeScript は C#よりもかなり柔軟な型の表現を持ちます。これはもともと動的な JavaScript の表現を可能なかぎりカバーするために、豊富な表現が可能になっています。
- SubType
- Union Type
- Generics
- 値型(いわゆる存在型とはちょっと違います)
TypeScript コンパイラの機能は、次の 3 つに分類することが出来ます
- 静的な型のチェッカー
- 型の除去
- ES2015 から ES5 への変換
TypeScript ユーザーの知るべきこととして、TypeScript の型は、JS への変換時にはただ取り除かれるだけです。型によってランタイムの処理が変わることはありません。
// before
const x: number = 1;
class C implements Base<K> {
private foo(): void {
return;
}
}
// after
const x = 1;
class C {
foo() {
return;
}
}
とはいっても、ランタイムへの影響が出る例外が 2 つあります。
- 非標準な enum 機能。実際に値を生成するので、ランタイムに関与します。
- 非標準な namespace 機能。ES Modules が策定されるより前の機能なので、基本的に使う必要はありません。
標準ライブラリや外部ライブラリなどは ジェネリクスとともに表現されることがあります。
// promise の例
async function fetchFoo(): Promise<string> {
const res = await fetch('/api/foo')
return res.text()
}// react の例
import React from 'react'
export default class MyApp extends React.Component<{a: number}, {b: number}> {
render() {
return <div>app</div>
}
}
エコシステム編
という章を頑張って書こうと思いましたが、もうこのあとは試行錯誤なので、いくつかポインタを置いておくだけにします。
- TypeStrong/ts-loader: TypeScript loader for webpack
- 最新版TypeScript 3.0+Webpack 4の環境構築まとめ(React, Vue.js, Three.jsのサンプル付き) - ICS MEDIA
- 特別なツール不要! TypeScript 2時代の型定義ファイルの取り扱い方 - Qiita
チーム開発でやる場合、社内の強い人を呼んでくるとか、僕みたいなフリーランス呼んでください。誰かが一度書けば、だいたい1年はメンテナンスフリーで回ります。
運用編、あとで書くかも。