typescriptをまじめに勉強することにした

typescriptをまじめに勉強することにした

理由

型が欲しい。

いままでも定点観測の範囲で目にした記事を幾つか取りだし眺めるというようなことはしていた。 読むことはそれなりに難しくなく書かれている記事の内容はその場では理解できてはいたような気がする。 一方で、いざ書こうとした時にはもう少し勉強が必要だと感じた。何がどこまでできるのかという制限やある機能を実現したい場合にはどのようにするかといった定石のようなものが頭の中には入っていない状態。

必要なこと

プログラミングに関する基本的な読み書きの勉強で必要なことは以下の2つだと思っている。

  • どれだけ問題を細分化できるか
  • どこまで細分化すれば十分だと判断できるか?

この2つができるようになっていればあとは自然と時間の経過と共に詳しくなっていくはずという楽観的な見方。

さしあたって必要なこと

さしあたって必要なことは以下の様なものかもしれない

  • 環境作成
  • 個人的なsandbox環境の作成

環境作成は、「勉強したい言語を書く。実行してみる。実行結果を確認する」というような試行錯誤のサイクルを手軽に回せる環境を作るということ。 sandbox環境は過去に作った環境の内容を保管しつつ、他の環境には影響を与えにくいように管理する仕組みの作成

環境作成

環境作成で考えるコトはサイクルを回すということ。それぞれのレベルで試行錯誤が手軽にできるような環境が作成できると良い。

  • 実行環境の作成
  • 1ファイルで言語上の機能を試す環境の作成
  • 複数のファイルに分割したコードを実行する環境の作成
  • unittestを試せる環境の作成

今のところここまでだけれど。後でもう少し増やしていく予定。

実行環境の作成

実行環境の作成は処理系が何らかのコードを実行出来るような環境が作成できれば十分そう。hello worldが実行できれば十分だと思う。

typescriptをnpmでinstallする程度で良い模様。

npm install typescript
vi hello.ts
./node_modules/.bin/tsc hello.ts

hello.ts

const target = "world";
console.log(`hello ${target}.`);

毎回 npm を手動でインストールするというのは面倒なため。この辺りの環境構築を手軽にしておきたい。以下で十分なようにpackage.jsonを調整する。

npm install

これ自体は以下の様なコードを雑に実行するだけで良い。

npm init
npm install --save-dev typescript

npm run scripts越しにコマンドを実行しようとした場合にはパスが通っているのでscriptsに何か書くというのが良いかもしれない。

  "scripts": {
    "build": "tsc *.ts || true"
  },

エラーメッセージの表示を見やすくするために || true を加えている。

1ファイルで言語上の機能を試す環境の作成

幾つかの機能を試す際に手軽に1ファイルにコード片を書き実行結果を確認すると言うことがしたい。 これはemacs上でquickrunを使うことで達成された。

以下のようなファイルを開いている最中で、C-c C-@ を押下するとquickrun越しにtscが実行され出力されたjsがnodeによって実行される。

// hello.ts
const target = "world";
console.log(`hello ${target}.`);

複数のファイルに分割したコードを実行する環境の作成

複数のファイルに分割したコードを実行する環境の作成と言ってもこれも幾つか種類があるかもしれない。例えば以下の様な種類がある。

  • 自分で作成した外部ファイルを読み込む
  • 型定義ファイル(.d.ts)が存在する外部ライブラリを読み込む
  • 型定義ファイル(.d.ts)が存在しない外部ライブラリを読み込む

自分で作成した外部ファイルを読み込む

基本的には、内部モジュールを使わず外部モジュールだけを使う形で良い。

src以下にtypescriptのファイルを書き lib以下にcompileされたjsを出力する

src/main.ts

{X} import from {x}
const x = new X();
x.say()

src/x.ts

export class X{
    constructor(){}
    say(){
        console.log("hai");
    }
}

node.jsを使って実行結果を確認したいので以下の様なオプションでコンパイルすることにする

tsc -t es5 -m commonjs --noImplicitAny --noEmitOnError --moduleResolution node src/*.ts --outDir lib
node lib/main.js

繰り返し使えるようpackage.jsonに書いておく

{
  "name": "tmp",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "tsc -t es5 -m commonjs --noImplicitAny --noEmitOnError --moduleResolution node src/*.ts --outDir lib || true"
  },
  "devDependencies": {
    "typescript": "^1.7.3"
  }
  "author": "",
  "license": "ISC"
}

tscに渡すオプションはtsconfig.jsonに書くことができるがこの時点では考えないことにする。

型定義ファイル(.d.ts)が存在する外部ライブラリを読み込む

試しにnode.js内のモジュールを使ってみることにする。

以下の様なコードにmain.tsを書き換えてみる。

import {X} from "./x";
const x = new X(process.argv[1]);
x.say()

もちろんprocess moduleが見つからないといって失敗する。

npm run build
src/main.ts(2,26): error TS2307: Cannot find module 'process'.

型定義などは個別に型定義ファイル(.d.ts)を書くことで対応できるが。.d.tsを書くのがだるい。使えるものは使いたいのでdtsmを使うことにする。

npm install --save-dev dtsm
dtsm init
dtsm search node/node.d.ts
dtsm install node/node.d.ts
tree typings
typings
├── bundle.d.ts
└── node
    └── node.d.ts

1 directory, 2 files

typings以下に取得した型定義ファイルが収集されるのでとりあえずbundle.d.tsを利用するようにすれば良さそう。

  "scripts": {
    "build": "tsc -t es5 -m commonjs --noImplicitAny --noEmitOnError --moduleResolution node src/*.ts typings/bundle.d.ts --outDir lib || true"
  },

入力にbundle.d.tsを渡してcompileすると実行可能になる。

npm run build
node lib/main.js foo
foo: hai

型定義ファイル(.d.ts)が存在しない外部ライブラリを読み込む

型定義ファイル(.d.ts)が存在しない外部ライブラリの場合には自分で.d.tsを書かないとダメ。 例えば先程のprocessを使えるようにするためには以下のような型定義ファイルを書く。

interface Process {
  argv: string[];
}
declare var process: Process;

何も考えたくない時にはAny(ただしnoImplicitAnyで禁止されているかもしれない)。

declare var process: Any;
moduleがある場合はどうするか

requireにより読み込まれるようなmoduleへの対応はどうするかというとdeclare moduleを使えば良さそうだった。

例えば以下の様なコードが実行したい場合。

import http = require('http')
http.createServer(function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('Hello World\n');
}).listen(1337, '127.0.0.1');

以下のような.d.tsを書く。

declare module "http" {
    export interface IncomingMessage extends events.EventEmitter, stream.Readable {
    }
    export interface ServerResponse extends events.EventEmitter, stream.Writable {
    }
    export interface Server extends events.EventEmitter {
        listen(port: number, hostname?: string, callback?: Function): Server;
    }
    export function createServer(requestListener?: (request: IncomingMessage, response: ServerResponse) =>void ): Server;
}

追記: requireは排除できそう。

requireを使う必要はなかった模様。 namespaceを直接importできる

namespace自体をimportするには以下の方法で書ける。

import * as http from "http";

特に相対パスを利用したimportに限らずnode modulesへの参照も関知してくれる(moduleResolutionがnodeなら)。

https://github.com/Microsoft/TypeScript/issues/2338 より

以下のような順序でパスは探索される。

  1. ambient module declarationが存在(これは declare module で定義したもののこと)すればこれを使う
  2. "./" や "../" で始まる場合は、この相対的な位置に存在するファイルを読み込む
  3. そうでなければ node_modules を探索する
  4. それでもなければ not found error

unittestを試せる環境の作成

とりあえず以下の様な方針。

  • いろいろなライブラリに依存したくない
  • .d.tsを管理したくない

なのでライブラリ部分はtypescriptで書くがテストはjavascriptで書くことにする。

またgistにuploadすることを考えるとフラットな構造のものの方が嬉しいかもしれない。 一方でテストランナーは欲しいのでmochaを使うことにする

npm init
npm install --save-dev mocha typescript
vi add.ts
./node_modules/.bin/tsc -t es5 -m commonjs add.ts
vi test.js
./node_modules/.bin/mocha --reporter=dot test.js
tsc -t es5 -m commonjs add.ts
mocha --reporter=dot test.js

add.ts

export function add(x:number, y:number): number {
    return x + y;
}

test.js

var assert = require('assert');

it("add 3 + 3 = 6", function(){
  var add = require('./add').add;
  assert.strictEqual(add(3, 3), 6);
});

これらをpackage.jsonで利用できるようにしておく。

{
  "name": "tmp2",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "tsc -t es5 -m commonjs add.ts",
    "test": "mocha --reporter=dot test.js"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "mocha": "^2.3.4",
    "typescript": "^1.7.5"
  }
}

https://gist.github.com/podhmo/054187ba3caf7cfb3802