先日 (2015年11月20日) に行われたTypeScript速習会@Wantedly の発表内容です。実際にコードを書いてみるという形で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のプロジェクト管理に使われるファイル
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"noImplicitAny": true,
"inlineSourceMap": true,
"inlineSources": true
},
"files": [
]
}
詳しい説明はTypeScriptのWikiへ
tsconfig.json + atom-typescript
atom-typescriptを使う人はさらに
...
"filesGlob": [
"**/*.ts",
"!node_modules/**"
],
"compileOnSave": false,
"atom": {
"rewriteTsconfig": true
}
}
ビルドタスク
package.json の scripts フィールドを使う
npm run ...
で実行
{
...
"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
<!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
コードを書こう
簡単な描画
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();
"files": [
"src/index.ts"
]
atom-typescript だと自動で追加してくれます
コンパイル
npm run tsc
npm run watchify
- index.htmlを開く
描画結果
JavaScript (ES6) とおなじ?
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();
型はちゃんとついている (型推論)
const canvas = document.createElement("canvas");
canvas: HTMLCanvasElement
const ctx = canvas.getContext("2d");
変なメソッド呼び出しをすると…
ctx.wrongMethod(1, 2, 3);
error TS2339: Property 'wrongMethod' does not exist
on type 'CanvasRenderingContext2D'.
interface
interface Path {
x: number;
y: number;
draw(ctx: CanvasRenderingContext2D): void;
}
export = Path;
"files": [
"src/index.ts",
"src/Path.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);
描画結果
TypeScript の型とは?
TypeScript の型の基本は interface
interface
: 必要なプロパティ(メソッド)の集合
interface
の条件を満たすと代入が成功する
(ダックタイピング / 構造的部分型)
関数もinterfaceで表せる
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 で紹介されているこちらの方法 を使えば、ES6 スタイルを使っても特に問題ないことがわかった (今まで勘違いをしていた…)
tsconfig.json 設定
"compilerOptions": {
"target": "es5",
"module": "commonjs",
...
}
-
"module": "commonjs"
- Import Require を使う -> CommonJS へコンパイルするよう設定
-
"target": "es6"
で"module"
を指定しない- ES6 スタイルになる
class
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;
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;
const rect = new Rectangle(100, 100, 200, 300);
const circle = new Circle(300, 300, 50);
fillPath("#abc", rect);
fillPath("#cba", circle);
"files": [
...
"src/Circle.ts",
"src/Rectangle.ts"
]
描画結果
class も interface で表せる
class → interface型 かつ new呼び出しできる値
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を使って
function randomColor() {
return "#" + _.sample("0123456789ABCDEF".split(""), 6).join("");
}
やり方1: JavaScript の require
// 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: 自分で型情報を書く
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
import _ = require("lodash");
function randomColor() {
return "#" + _.sample("0123456789ABCDEF".split(""), 6).join("");
}
import require 構文で lodash を require
型定義が別に必要
型定義が集まっているリポジトリ DefinitelyTyped
tsd
型情報管理ツール 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に追加
"files": [
...
"typings/tsd.d.ts"
]
lodash.d.ts
declare var _: _.LoDashStatic;
declare module _ {
interface LoDashStatic {
sample<T>(collection: Array<T>, n: number): T[];
...
}
}
declare module "lodash" {
export = _;
}
実はstringも使える
import _ = require("lodash");
function randomColor() {
return "#" + _.sample("0123456789ABCDEF", 6).join("");
}
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
も渡せる
実際に使ってみる
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);
}