Hatena::ブログ(Diary)

雑草の音

2018-10-04

TypeScript入門以前ガイド - mizchi's blog 07:38

<title>TypeScript入門以前ガイド - mizchi's blog</title>

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 を推奨しています。

知っておくとよい雰囲気


ES2015 for ES5 Programmers

あなたは一昔前の JavaScript で生きてきた人間です。 jQueryAPI は一通り知っています。サーバーで生成した HTML にクラスをつけて $(...)セレクタを捕まえて、その中身を書き換えてきました。$.map(...) による制御や、 var self = this のようにクロージャの this を保存して再利用してきたかもしれません。

ES2015 入門するにあたっては、ES5 までに覚えた「お約束」は、全部忘れてください。


ES Modules

ES Modules(略称 ESM) は JavaScript に他の言語のようなモジュールシステムを導入する機能です。

import - JavaScript | MDN

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 というツールで、一つのファイルをエントリーにして、一つのファイルに固めてしまうことが現状ベストプラクティスとされています。

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

(やや難しいので必要になるまで読まなくて良い)

PromiseJavaScript で非同期処理を表現するための機能です。

「1 秒待つ関数」を表現するのは次のようになります。

const wait = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(), 1000);
  });
wait().then*1;


reject(...) を呼ぶと失敗処理になります

const willFail = () =>
  new Promise((resolve, reject) => {
    reject(new Error("reason..."));
  });

willFail()  .then*2  .catch*3;


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 の型は、基本的には JavaC# と似ています。Javaの影響を受けた C# の作者 Anders Hejlsberg が設計した JavaScript 拡張です。

入門資料

TypeScript in 5 minutes · TypeScript

Basic Types · TypeScript

typescript-ninja/typescript-in-definitelyland: TypeScript in Definitelyland ちょっと古い

バージョンごとの変更点は vvakame さんの Qiita が参考になります 「user:vvakame tag:typescript」の検索結果 - Qiita

TypeScriptC#よりもかなり柔軟な型の表現を持ちます。これはもともと動的な 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>
  }
}


エコシステム編

という章を頑張って書こうと思いましたが、もうこのあとは試行錯誤なので、いくつかポインタを置いておくだけにします。


チーム開発でやる場合、社内の強い人を呼んでくるとか、僕みたいなフリーランス呼んでください。誰かが一度書けば、だいたい1年はメンテナンスフリーで回ります。

運用編、あとで書くかも。

*1:) => console.log("wait done"

*2:) => console.log("never"

*3:) => console.log("come here"