TypeScript速習会


先日 (2015年11月20日) に行われた[TypeScript速習会@Wantedly](http://wantedly.connpass.com/event/22954/) の発表内容です。実際にコードを書いてみるという形でTypeScriptの使い方を紹介しました。

- [スライド(同じ内容です)](https://github.com/seanchas116/slide-learn-typescript)
- [サンプルプロジェクト](https://github.com/seanchas116/learn-typescript)


# 今日紹介すること

* TypeScriptプロジェクトのセットアップ
* 基本的なTypeScriptのコードを書く
* 既存のJavaScriptライブラリを使う


# TypeScript の紹介


## TypeScript とは?

* JavaScript + 静的型付け
  * (ほぼ) JavaScript の上位互換
* 最新JavaScript (ES2015以降) の文法も積極的に取り入れている

## TypeScript のうれしい所

* 型チェックでエラーを未然に発見
* エディタ上でのコード補完 / リファクタリング
* JavaScript / CoffeeScript などとの共存

## TypeScript のうれしくない所

* JavaScriptの悪い点も引き継いでいる
* `any`、`null`等による型安全性の穴

## TypeScript リンク

* Webサイト http://www.typescriptlang.org/
* GitHub https://github.com/Microsoft/TypeScript
* 言語仕様 https://github.com/Microsoft/TypeScript/blob/master/doc/spec.md

# 早速書いてみよう!

## 今日書く内容

Canvas要素を使ったお絵かきをTypeScriptで

# セットアップ

## 今回のプロジェクト構成

### TypeScript ファイル (.ts)

↓ TypeScriptコンパイラ (tsc)

### JavaScript ファイル (.js)

↓ Browserify (watchify)

### bundle.js


## package.json

```
mkdir learn-typescript && cd learn-typescript
npm init
```


## ツールのインストール

### TypeScript

```
# 安定版
npm install --save-dev typescript
# nightly版 (機能が多い / 冒険したい人におすすめ)
npm install --save-dev typescript@next
```

### watchify

```
npm install --save-dev watchify
```

## tsconfig.json

TypeScriptのプロジェクト管理に使われるファイル

```json:tsconfig.json
{
    "compilerOptions": {
        "target": "es5",
        "module": "commonjs",
        "moduleResolution": "node",
        "noImplicitAny": true,
        "inlineSourceMap": true,
        "inlineSources": true
    },
    "files": [
    ]
}
```

詳しい説明は[TypeScriptのWiki](https://github.com/Microsoft/typescript/wiki/tsconfig.json)へ

## tsconfig.json + atom-typescript

atom-typescriptを使う人はさらに

```tsconfig.json
  ...
  "filesGlob": [
      "**/*.ts",
      "!node_modules/**"
  ],
  "compileOnSave": false,
  "atom": {
      "rewriteTsconfig": true
  }
}
```


## ビルドタスク

package.json の scripts フィールドを使う  
`npm run ...` で実行

```json:package.json
{
  ...
  "scripts": {
    "tsc": "tsc -w",
    "watchify": "watchify -d src/index.js -o bundle.js"
  },
  ...
}

```

* `npm run tsc` (`tsc -w`)
  * TypeScriptを監視してコンパイル
* `npm run watchify` (`watchify -d src/index.js -o bundle.js`)
  * Browserify (watchify で監視してバンドル)

## index.html

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <title>TypeScript Canvas</title>
  </head>
  <body>
  </body>
  <script src="bundle.js"></script>
</html>
```


## 用意したファイル

* `package.json`
  * 全般的プロジェクト設定
  * 依存パッケージ設定
  * ビルドタスク設定
* `tsconfig.json`
  * TypeScriptのプロジェクト設定
* `index.html`
  * ブラウザでロードするHTML


# コードを書こう

## 簡単な描画

```ts:src/index.ts
const canvas = document.createElement("canvas");
canvas.width = 800;
canvas.height = 600;
document.body.appendChild(canvas);

const ctx = canvas.getContext("2d");

ctx.beginPath();
ctx.fillStyle = "#abc";
ctx.rect(100, 100, 200, 200);
ctx.fill();
```

```tsconfig.json
  "files": [
    "src/index.ts"
  ]
```

atom-typescript だと自動で追加してくれます


## コンパイル

* `npm run tsc`
* `npm run watchify`
* index.htmlを開く


## 描画結果

![hello.png](https://qiita-image-store.s3.amazonaws.com/0/36226/f966d340-dae0-be53-b651-ebd57b675e17.png "hello.png")


## JavaScript (ES6) とおなじ?

```ts
const canvas = document.createElement("canvas");
canvas.width = 800;
canvas.height = 600;
document.body.appendChild(canvas);

const ctx = canvas.getContext("2d");

ctx.beginPath();
ctx.fillStyle = "#abc";
ctx.rect(100, 100, 200, 200);
ctx.fill();
```

## 型はちゃんとついている (型推論)

```ts
const canvas = document.createElement("canvas");
```

canvas: [HTMLCanvasElement](https://developer.mozilla.org/ja/docs/Web/API/HTMLCanvasElement)

```ts
const ctx = canvas.getContext("2d");
```
ctx: [CanvasRenderingContext2D](https://developer.mozilla.org/ja/docs/Web/API/CanvasRenderingContext2D)

## 変なメソッド呼び出しをすると…

```ts
ctx.wrongMethod(1, 2, 3);
```

```
error TS2339: Property 'wrongMethod' does not exist
on type 'CanvasRenderingContext2D'.
```

# interface

```ts:src/Path.ts
interface Path {
  x: number;
  y: number;
  draw(ctx: CanvasRenderingContext2D): void;
}
export = Path;
```

```json:tsconfig.json
  "files": [
    "src/index.ts",
    "src/Path.ts",
  ]
```

```ts:src/index.ts
import Path = require("./Path");

function fillPath(color: string, path: Path) {
  ctx.beginPath();
  ctx.fillStyle = color;
  path.draw(ctx);
  ctx.fill();
}

const rect = {
  x: 100, y: 200, w: 100, h: 100,
  draw(ctx: CanvasRenderingContext2D) {
    ctx.rect(this.x, this.y, this.w, this.h);
  }
};

fillPath("#abc", rect);
```

## 描画結果

![rect.png](https://qiita-image-store.s3.amazonaws.com/0/36226/a437a377-1721-baf6-b11c-ac088a81d471.png "rect.png")

## TypeScript の型とは?

TypeScript の型の基本は `interface`

`interface`: **必要なプロパティ(メソッド)の集合**

`interface`の条件を満たすと代入が成功する  
(ダックタイピング / 構造的部分型)

## 関数もinterfaceで表せる

```ts
function foo(a: number, b: string) {
  return 100;
}

type FooType = typeof foo;
type FooType = (a: number, b: string) => number;
interface FooType {
  (a: number, b: string): number;
}
```

すべて同じ意味

## TypeScript のモジュール管理

* **ES6 スタイル** (新しい)
  * ES6 のモジュール構文を使う
* **Import Require** (昔からある)
  * CommonJS風モジュール構文
  * `import Foo = require("./Foo")`
  * `export = Foo`
  * 今回はこちらを使用

## TypeScript のモジュール: どちらを使う?

~~Import Require のほうが今のところ安心
  * 古くからある型定義ファイルは  
    こちらを前提にしている
  * ES6 スタイル と Import Require の互換性が低い
    * 今後改善される可能性あり~~

[TypeScript 1.5.3 変更点 - Qiita](http://qiita.com/vvakame/items/9b9fde6c71aae6a824c0#%E6%97%A2%E5%AD%98%E3%81%AE%E5%A4%96%E9%83%A8%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%ABor-%E5%9E%8B%E5%AE%9A%E7%BE%A9%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92es6-module-syntax%E3%81%A7%E6%89%B1%E3%81%86) で紹介されている[こちらの方法](http://qiita.com/vvakame/items/9b9fde6c71aae6a824c0#%E6%97%A2%E5%AD%98%E3%81%AE%E5%A4%96%E9%83%A8%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%ABor-%E5%9E%8B%E5%AE%9A%E7%BE%A9%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92es6-module-syntax%E3%81%A7%E6%89%B1%E3%81%86) を使えば、ES6 スタイルを使っても特に問題ないことがわかった (今まで勘違いをしていた…)

## tsconfig.json 設定

```tsconfig.json
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    ...
  }
```

* `"module": "commonjs"`
  * Import Require を使う -> CommonJS へコンパイルするよう設定
*  `"target": "es6"` で `"module"` を指定しない
  * ES6 スタイルになる

# class

```ts:src/Circle.ts
class Circle {
  constructor(public x: number, public y: number, public r: number) {}
  draw(ctx: CanvasRenderingContext2D) {
    ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2, true);
  }
  static unit() {
    return new Circle(0, 0, 1);
  }
}
export = Circle;
```


```ts:src/Rectangle.ts
class Rectangle {
  constructor(public x: number, public y: number, public w: number, public h: number) {}

  draw(ctx: CanvasRenderingContext2D) {
    ctx.rect(this.x, this.y, this.w, this.h);
  }
}
export = Rectangle;
```

```ts:src/index.ts
const rect = new Rectangle(100, 100, 200, 300);
const circle = new Circle(300, 300, 50);
fillPath("#abc", rect);
fillPath("#cba", circle);
```

```json:tsconfig.json
  "files": [
    ...
    "src/Circle.ts",
    "src/Rectangle.ts"
  ]
```

## 描画結果

![rectcircle.png](https://qiita-image-store.s3.amazonaws.com/0/36226/f6b157be-e7a1-8129-a421-c2ca75d05a5d.png "rectcircle.png")


## class も interface で表せる

class → **interface型** かつ **new呼び出しできる値**

```ts
interface Circle {
  x: number; y: number; r: number;
  draw(ctx: CanvasRenderingContext2D) void;
}
interface CircleStatic {
  new (x: number, y: number, r: number): Circle;
  unit(): Circle;
}
var Circle: CircleStatic = ...
```


# JavaScriptライブラリを使う

## 例: ランダムな色をつくりたい

[lodash](https://lodash.com/)を使って

```ts
function randomColor() {
  return "#" + _.sample("0123456789ABCDEF".split(""), 6).join("");
}
```

## やり方1: JavaScript の require

```ts
// require() 関数の型定義
declare function require(module: string): any;

const _ = require("lodash");
// _: any (制約のない型)

function randomColor(): string {
  // anyなのでどんなメソッドも呼べる
  return "#" + _.sample("0123456789ABCDEF".split(""), 6).join("");
}
```

JavaScript (CommonJS) の require を直接使う

ライブラリは `any` 型で扱う

## やり方2: 自分で型情報を書く

```ts
const _: LodashStatic = require("lodash");

declare function require(module: string): any;

interface LodashStatic {
  sample<T>(array: T[], count: number): T[];
}

function randomColor() {
  // 型チェックされてOK
  return "#" + _.sample("0123456789ABCDEF".split(""), 6).join("");
}
```

interface を使って自分で型情報を書く

## やり方3(本命): DefinitelyTyped

```ts
import _ = require("lodash");

function randomColor() {
  return "#" + _.sample("0123456789ABCDEF".split(""), 6).join("");
}
```

import require 構文で lodash を require

**型定義が別に必要**

型定義が集まっているリポジトリ [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped)


## tsd

型情報管理ツール [tsd](https://github.com/DefinitelyTyped/tsd) を使う

```
npm install tsd -g
tsd init # tsd.json 作成
tsd install lodash --save
```

```
typings
├── lodash
│   └── lodash.d.ts
└── tsd.d.ts
tsd.json
```

DefinitelyTyped から型情報を取ってきて管理する


## tsconfig.jsonに追加

```json:tsconfig.json
  "files": [
    ...
    "typings/tsd.d.ts"
  ]
```


## lodash.d.ts

```ts
declare var _: _.LoDashStatic;

declare module _ {
  interface LoDashStatic {
    sample<T>(collection: Array<T>, n: number): T[];
    ...
  }
}

declare module "lodash" {
  export = _;
}
```


## 実はstringも使える

```ts
import _ = require("lodash");

function randomColor() {
  return "#" + _.sample("0123456789ABCDEF", 6).join("");
}
```

```ts
declare module _ {
  interface List<T> {
    [index: number]: T;
    length: number;
  }
  interface LoDashStatic {
    sample<T>(collection: List<T>, n: number): T[];
    ...
  }
}
```

`string`は`List<T>`を満たすので、  
`_.sample`に`string`も渡せる


## 実際に使ってみる

```ts:src/index.ts
function randomColor() {
  return "#" + _.sample("0123456789ABCDEF", 6).join("");
}

const circles  = _.times(200, () => ({
  fill: randomColor(),
  path: new Circle(Math.random() * 800, Math.random() * 600, 10)
}));

for (const {fill, path} of circles) {
  fillPath(fill, path);
}
```

## 描画結果

![random.png](https://qiita-image-store.s3.amazonaws.com/0/36226/06d8fbca-e732-b6d9-4205-f94344450300.png "random.png")