TypeScriptを学ぶ
Posted
初めてTypescriptに触った時に書いたメモ。
概要
そもそもtypescriptはjavascriptにクラスベースのオブジェクト思考を持ち込んだもので、コンパイルすることによってjavascriptのソースコードを吐き出してくれる。これは、クライアントサイドでも、サーバーサイド(NodeJS)でも使用できる。(ただのjavascriptのソースなので)
つまり、typescriptはコンパイラを入れることで開発環境を構築する。
環境構築
Node.jsのインストール
これは生成したスクリプトの実行場所としてNode.jsを使用するという意味である。
rubyでいうところのrbenvのようなものがNode.jsにもある。nというライブラリだ。
npm
Node.jsをインストールすると、nvmとnpmコマンドが使えるようになる。nvmはjavaでいうjvmのようなもの、npmはパッケージ管理ソフトになる。
npm init
npmにはグローバルな環境にパッケージをインストールする-gオプションがあるが、基本的には-gオプションを取ってローカルにインストールすれば良い。このとき、開発時に必要なだけのライブラリは-D(--save-devのエイリアス)を付けることで、後述するpackage.jsonに開発時のみで依存するパッケージであると明記される。
ローカルにインストール、もっと言えば、プロダクトと毎にインストールすることになるわけだが、こうすることでプロダクト毎にライブラリのバージョンを管理できる。(嵩むが)
その時、最初に行うのがnpm initコマンドで、対話型で情報を入力するとpackage.jsonというファイルを出力してくれる。rubyのbundlerで言うところのgemspecファイルみたいなもの。このファイルがないと、ローカルインストールしたときに警告が出る。これは、ローカルインストール=プロダクト毎に管理しているということをnpmが意識しているのだと思う。
node_module ディレクトリ
ローカルにインストールすると、node_modulesディレクトリから始まることが分かる。これは、例えば同じ場所で別のnpmパッケージをインストールすると、やはりnode_moduleに入れられる。
もし、グローバル(-g)オプションを付けたら、/usr/local/libディレクトリにnode_moduleディレクトリが生成され、そこに入れられる。
グローバルの場合は、さらにこの中にあるスクリプトを呼び出すスクリプトが/usr/local/binに生成
されるということだと思う。
コマンド系ライブラリ
ローカルにインストールして管理するのは良いが、グローバルな環境にパッケージをインストールするときと違い、ローカルにインストールするとライブラリは良いがコマンド群はそのままでは使えない。グローバルにインストールすると/usr/local/binディレクトリにスクリプトがインストールされるが、ローカルだとローカルインストールしたカレントディレクトリに.binディレクトリが生成され、そこにコマンド実行用スクリプトが入れられる。
なので、npm run <コマンドファイルのパス>で実行しないといけなくなるわけだが、これを簡単に実行してくれるものがnpmにバンドルされるようになっている。npxという。
npxは、 カレントディレクトリ/.binにあるnpmパッケージのコマンドを実行する 又は、 存在しないコマンドでアレば一時的にインストールして実行する 機能を持ったコマンド。コレを使うには、単純にプロダクトのルートディレクトリ上で
$ npx コマンド名
と打てば良い。
コンパイラのインストール
typescriptはnpmからインストール出来る。
$ npm install -D typescript
実行用コマンドをインストール
現状だと作成したtypescriptソースをコンパイルして実行しするまでの手順は下記のようになる。
- typescriptでソースを書く
- tsc(コンパイラ)でtsファイルをjsファイルに変換する
- jsファイルをnodeコマンドで実行する
だが、だるい。なので、ts-nodeというコマンドをインストールする。これを使えば、tsファイルをそのまま実行できる(tsファイルを指定すれば2,3の動作をまとめて行うような動作をしてくれる)。
$ npm install -D ts-node
typescript触ってみる
基本的にjavascriptで表現できるものはtypescriptでも同様に表現でき、それらにtypescriptならではの表現の許可が追加されているのだと考えれば良い。
タイプアノテーション
function function_name( var: string )
typescriptでは、上記のように方のアノテーションを指定できる。(stringのほう) 引数とかでなくても指定できる。
変数
var,let,constがある。これはjavascriptと同じ。
varはスコープを持たないjavascriptに最初からあった変数。なので、使いにくい。他の言語で言うグローバル変数のように振る舞うので、関数の中だろうがその中のブロック内だろうがアクセスされる。 ただし、varは関数内で再定義すると、その関数内では別の変数を新たに宣言したように振る舞い、関数から外に出ると再び別の場所で代入された値に戻る
var str:string = "this is var str"
function demo_a(){
if(true){
console.log( str ) // => this is var str
}
console.log( str ) // => this is var str
}
demo();
console.log( str ) // => this is var str
上記だとただのグローバル変数のように振る舞う。
var str:string = "this is var str"
function demo_a(){
if(true){
var str:string = "this is re-declare var str"
console.log( str ) // => this is re-declare var str
}
console.log( str ) // => this is re-declare var str
}
demo();
console.log( str ) // => this is var str
上記だと関数内だけ、新たに宣言されたように振る舞う。しかし、ここで…
var str:string = "this is var str"
function demo_a(){
// falseにかえると...
if(false){
var str:string = "this is re-declare var str"
console.log( str )
}
console.log( str ) // => undefined
}
demo();
console.log( str ) // => this is var str
何故か関数内がundefinedになってしまう。関数内でvarを再定義するのは基本的にやめたほうが良さげ。素直にグローバルとしてどこかで定義してそれをずっと使いまわし、ほかは後述するletとconstを使用するのが良い。
letはその変数が宣言されたブロックにスコープが制限される変数。
constはスコープの制限はletと同じだが、定数になる。スコープが制限されているもので、一時的に使われるだけのもののため、基本的には特別再代入されるわけではない変数はこのconstを使おうというお作法があるらしい。
再代入が必要なのは、例えばfor文で使われるカウンタのような変数。
- 基本的には
const - 再代入必要であれば
let - グローバルとしては
var
らしい。
変数letの性格
変数letは同じスコープ内で同じ変数を宣言するとコンパイラがエラーを吐くようになっている。しかし、下記のような宣言は許される。
let num:number = 1;
function demo(){
let num:number = 2;
if(true){
let num:number = 3;
console.log( num )
}
console.log( num )
}
console.log( num )
demo();
特に変なのはfunction内のifの中と外の関係だろう。スコープが変われば同じ名前の変数を宣言できてしまう。これは、コンパイルすれば分かるが変数_1,変数_2のように名前をコンパイラが付け足して解決している。
データ型
typescriptはjavascript同様でデータ型を管理している。
- String
- Boolean
- Array
- Tuple
- preserveConstEnumsAny
- Union
- Any
- Void
- Never
そしてそれらは関数を保持している。
型推論 (Type inference)
typescriptはデータ型を指定しなくても型推論を行うので、例えば下記のようなものはコンパイルエラーになる。
var a = "string";
var b = 1;
a = b // コンパイルエラー
キャスト=型アサーション (Type Assertion)
any型で捉えた変数をキャストしたいときなど、下記のように書くことでキャストできる。
let a: any = 123
let b = <number> a
上記でキャストが無い場合、bの方はanyになるが、キャストされているのでnumberになる。
関数の戻値
// コレが戻値
function func() : string {
...
}
アロー関数(Arrow Function)
ラムダ式のようなもの
(param1 , param2 ...) => { 処理 }
一行で書ききれるならブロックはなくても良い。
interface
typescriptのinterfaceは変数と関数の雛形を定義できる構造体であり、クラスによって実装させることも出来る(javaのように)。直接関数を中に定義することはできない。
interface InterfaceHuman{
name: string;
age: number;
say: (other) => void;
getName(): number;
}
割とややこしいので一個ずつ書いていく。
変数の型を定義するのに利用する
シンプルに、構造体として利用するパターン。
interface Human{
name: string;
age: number;
}
let alice: Human = { name: "alice", age: 18 };
console.log( alice.name + alice.age )
関数の型を定義するのに利用する
interface SomeStrFunc{
// ( 引数の型 ): 戻り値の型;
( string ): string;
}
function addHello( name: string ): string {
return "Hello, " + name
}
let ssf: SomeStrFunc = addHello;
console.log( addHello( "alice" ) )
javascriptは変数に引数なし関数を代入すると関数オブジェクト的なものが作れる。その型をインターフェースで定義できる。
配列の型を定義するのに利用する
interface NumList{
[ index: number ]: number;
}
const numArr: NumList = [1,2,3];
console.log( numArr[0] )
console.log( numArr[1] )
console.log( numArr[2] )
[ index: number ]: numberがキモ。次に書く。
インデックスシグネチャ
[ index: <インデックスの型(string又はnumberのみ指定可能)> ]: <値(戻り値)の型>はインデックスシグネチャと呼ばれ、下記のように使える。
interface BoolList{
[ index: number ]: boolean;
}
const boolArr: BoolList = {};
boolArr[0] = true
boolArr[1] = false
console.log( boolArr[0] ) // true
console.log( boolArr[1] ) // false
console.log( boolArr[2] ) // undefined
上記のnumberの場合。下記はstringの場合
interface BoolList{
[ index: string ]: boolean;
}
const boolArr: BoolList = {};
boolArr["flag1"] = true
boolArr["flag2"] = false
console.log( boolArr["flag1"] ) // true
console.log( boolArr["flag2"] ) // false
console.log( boolArr["flag3"] ) // undefined
ちなみに、下記のうようなことも可能。
interface Test{
[ index: string ]: Function;
}
const ti: Test = {};
ti["func"] = function(){ console.log("call function") };
ti.func();
インデックスシグネチャで動的に関数を追加できてしまう。インデックスシグネチャは型安全を崩壊させるとかいうのはコレを言っているのかな?
オプショナルプロパティ
インターフェースに定義される方をプロパティと呼ぶが、あってもなくても良い任意のプロパティも指定可能。プロパティ名の後ろに?をつければ良い。
読み込み専用プロパティ
プロパティ宣言時、先頭にreadonlyをつければ良い。
継承
extendsで継承可能
実装
implementsでclassによる実装が可能。この辺はjavaによく似ている。
複合型
以上の要素を複合して型を定義することも出来る。
interface Human{
name: string;
age: number;
say( string ): void;
}
function say( name: string ): void {
console.log( "hello, " + name );
}
const alice: Human = { name: "alice", age: 19, say };
console.log( alice.name );
console.log( alice.age );
alice.say("bob");
上記の場合、Human型は…
nameというstring型のプロパティageというnumber型のプロパティ- 引数の型が
string,戻り地がvoidのsayと名付けられた関数
を持つ型であると定義できる。
class
やっとclassにたどり着いた。
classは…
- コンストラクタを持ち
- プロパティを保持でき
- メソッドを定義できる
型の宣言であると言えそう。
インターフェースはイニシャライザを持たず、あくまで型の宣言だけが出来るものであり(これはjavaのインターフェースに通ずるものがある)ププロパティに値を持たせて保持させることや、関数を直接実装して保持させることができなかったが、classにはイニシャライザが存在するので、直接値や関数を保持させることが出来る。
class TeenHuman{
name: string;
age: number = 19;
constructor( h_name: string ){
this.name = h_name;
}
say( other ): void {
console.log( "hello, " + other );
}
}
const alice = new TeenHuman( "alice" )
console.log( alice.name );
console.log( alice.age );
alice.say("bob");
宣言した場で値を放り込むことも出来るし、コンストラクタ経由で値を放り込むことも出来る。関数も直接実装できる。
特に何も指定しなければアクセサはpublicになる。
メソッドのオーバーライド
継承した親クラスのメソッドのオーバーライドは可能。
メソッドのオーバーロード
Rubyのように、オーバーロードは不可。
クラスの属性
abstractで抽象クラスを定義できる。
Data Modifiers(要はアクセス属性)
public,private,protectedがある。おなじみのやつ。意味はjavaと似たようなものだと思う。
static属性
おなじみのstaticもある。クラスに定義を行う。javaと似たようなものだと思う。
モジュールの利用
当然だが、外部ファイル、モジュールの利用が出来る。
後述するnamespaceはこのモジュール内では使ってはいけないらしい。
exportとimport
あるファイルで定義された変数やクラスを他のファイルで使用したいとき、importとexportを使用する。
module.ts
export const greeting: string = "Hello";
test.ts
import { greeting } from "./module"
console.log( greeting )
javascriptでは、requireによって別のファイルをインスタンス化してしまい、そのインスタンス経由で別のファイルでexport指定を受けた変数や関数を取り出すことが出来る。これを、typescriptではimportとexportで簡潔に表現できる。
exportの指定を受けていない変数や関数は別のファイルでimportしてもアクセスできない。
2つのexport
exportには 名前付きexport と デフォルトexport がある。
名前付きexport
exportしたいものを具体的に指定してexportする方法。上記で書いたexportはこれ。通常はコレを使う。変数やクラスなどの宣言時にプレフィックスとしてexportをつける方法や、既に宣言したものをまとめて指定することも出来る。
export class Something { ... }
export { /** 既に宣言されている変数やクラスや関数を、","で区切って指定 **/ };
export { somefunc as funcA }; // asを使うことでエイリアスをつけることが出来る
他にもある。
デフォルトexport
特に名前を指定せずにimportされたときに指定されるものを、ひとつだけデフォルトエクスポートとして定めることが出来る。
module.ts
export default function func( str: string ): void {
console.log( str );
}
test.ts
import def from "./module"
def( "something to put" );
上記の場合、特に名前を指定していないのでexport defaultで指定された関数がdefに格納される。importの後ろに付けた名前は変数となり、上記の場合、defは関数部ジェクトのように振る舞うことになる。
namespace
名前空間も存在するが、ruby等のnamespaceとは少し雰囲気が違う。
namespace SomeNamespace {
...
}
下記コードを見れば分かるが、まるでmoduleのような感じになる。実際、もともとnamespaceは 内部モジュール と呼ばれていたものらしい。exportが無い場合、外からアクセスができない。
namespace SomeNs {
export function func(){
console.log("call SomeNs::function");
}
}
SomeNs.func();
classの定義も出来る。exportを忘れずに。
namespace SomeNs {
export function func(){
console.log("call SomeNs::function");
}
export class InnerClass {
func(){
console.log("call SomeNs::InnerClass.func");
}
}
}
SomeNs.func();
const nsIc = new SomeNs.InnerClass();
nsIc.func();
複数ファイルに分解するためのnamespace
namespaceはmoduleに挙動が似ているが、使い方が違う。moduleが独立した外部ライブラリのように振る舞うのに対し、namespaceはCのヘッダファイルのように振る舞う。どういうことかと言うと、コンパイル時に他のファイルにインクルードされ、mixinされるイメージだ。
例えば下記のような2つのファイルがあったとする。
SomeNe.ts
namespace SomeNs{
export function func(): void{
console.log("call SomeNs::function")
}
}
Test.ts
/// <reference path="SomeNs.ts" />
SomeNs.func();
これをコンパイルするとき、通常なら2つのファイルが出力されるが、namespaceを使用するときはSomeNs.tsがTest.tsにmixinされるので、一つのファイルとして出力される。
これをコンパイルするときは、その旨をコンパイラに伝えてあげないといけない。具体的には下記のようになる。
$ tsc --outFile Test.js Test.ts SomeNs.ts
これで、2つのファイルをコンパイラが一つにまとめてくれる。
namespaceAをreferenceして、拡張したnamespaceBがあったとして、それを使いたいときは、全てのtsファイルをコンパイラに渡してあげれば、コンパイラが勝手に順序を判断してmixinしてくれる。
なので、モジュールは独立したライブラリとしての利用、ネームスペースは巨大な一つの構造を分離して管理する記法としての利用、と考えれば良さげ。
型定義ファイル
***.d.tsというファイルがあればそれは型定義ファイルと呼ばれるものだ。型定義ファイルとは外部ライブラリのAPIを使用するときに引数や戻り値の型をtypescriptコンパイラに教えてくれるもの。コレがないと(ただのjsファイルのみだと)コンパイラはライブラリの関数が受け取るべき型や戻り値の型を判断できない。
typescript2.0以前は型定義ファイル管理ツールをnpmなどでインストールしてそれを使わないといけなかったが、2.0以降はそれらは不要になった。
ダックタイピング
typescriptはダックタイピングが可能。だが、rubyのように後からプロパティを生やすのは、やらない。(それをすると、型を壊してしまう)
class TestClass {
str: string;
constructor( s: string ){
this.str = s;
}
getStr(): string {
return this.str;
}
}
const tc = new TestClass("this is original");
console.log( tc.getStr() );
// => this is original
tc.getStr = function getStr(): string { return "this is not original" };
console.log( tc.getStr() );
// => this is not original
npmによってインストールされたモジュールはどのようにして読み込まれるのか
npmを利用してnode_modulesディレクトリにダウンロードされたtsライブラリは、一般的には下記のようにして利用することができる。
import { SomeClass } from "lib_name"
これによって、lib_nameライブラリのSomeClassクラスが利用可能になる。これはどういう仕組みになっているのか。
ここで、下記のようなツリー構造があるとする。
root_dir
├ model
│ ├ tx.ts
│ ├ block.ts
│ └ node.ts
└ controller.ts
このとき、controller.tsはmodelディレクトリの中の全てのtsファイルをインポートしたいとする。普通ではれば、
import { 使いたいクラス群など } from "./model/tx.ts"
import { 使いたいクラス群など } from "./model/block.ts"
import { 使いたいクラス群など } from "./model/node.ts"
としなければいけない。これだと大変なので、typescriptにはこれを回避する仕組みが組み込まれている。
typescriptの中でindex.tsという名前のファイルは特別なファイルで、イインポート時にディレクトリの指定だけしかないとき、typescriptはindex.tsを探しに行く。ここに、エクスポートをまとめて書いておけばディレクトリの指定だけでまとめてエクスポート対象に出来るようになる。どういうことかというと、
root_dir
├ model
│ ├ tx.ts
│ ├ block.ts
│ ├ node.ts
│ └ index.ts
└ controller.ts
index.ts
export { exportしたいクラス群など(ワイルドカードでも) } from "./tx.ts"
export { exportしたいクラス群など(ワイルドカードでも) } from "./block.ts"
export { exportしたいクラス群など(ワイルドカードでも) } from "./node.ts"
のようなindex.tsを加えて、controller.tsの中で、
import { 使いたいクラス群 } from "./model"
とするだけで良い。importすると、指定の名前のパスを調べて、それがディレクトリであればindex.tsを探すようになっているようだ。
さらに、指定が相対パス、絶対パスでなければ、node_modules内にその名前のディレクトリがないかを探索している。つまり、
npm installでnode_modulesディレクトリにライブラリが入るimportでライブラリのディレクトリ名だけ指定されて、mode_modules内にその名前のディレクトリがないか調べる- tsファイルの指定が無いので、
index.tsが無いか調べる
以上によって、最初の記述によって外部ライブラリが利用できるようになる。
コンパイル
コンパイル自体は
$ tsc <file name>
コンパイルの出力先設定
コンパイルするだけなら上記で可能だが、これだとカレントディレクトリに生成してしまう。コンパイル先は指定したい。
このとき、tscのオプションで済ませたい場合は--outFileオプションを利用する。
$ tsc --outFile <出力ファイルのパス> <tsソースファイル>
しかし、コレに限らずコンパイル時にひとつひとつ情報をコンパイラに渡してあげるのは大変。
tsconfig.json
上記 コンパイルの出力先設定 で書いたように、コンパイルオプションを一回一回渡してやるのは大変。そこで、tsconfg.jsonと呼ばれるjsonファイルを作る。これによって煩わしさを解決する。
tscコマンドは、tsconfig.jsonは入力ファイルを指定せずに使用されると(tscだけで実行すると)、カレントディレクトリからtsconfig.jsonファイルを探し、そこに書いてある内容に従ってコンパイルを実行する。
もし、カレントディレクトリにtsconfig.jsonが無い場合、親ディレクトリに登って探し、更にそこでも見つからない場合、どんどんその親へ親へとディレクトリを登ってtsconfig.jsonを探査する。
tsconfig.jsonが置かれるのは基本的にはそのプロダクトのルートディレクトリで、tsconfig.json内に記入した相対パスはtsconfig.jsonファイルの存在する場所からの相対パス、つまり、プロダクトのルートディレクトリを基準とした相対パスになる。
下記に示すような内容になっている。
{
"compilerOptions": {
"module": "commonjs",
"noImplicitAny": true,
"removeComments": true,
"preserveConstEnums": true,
"sourceMap": true,
"rootDir": ".",
"outDir": "./build/"
},
"files": [
"core.ts",
"sys.ts",
"types.ts",
"scanner.ts",
"parser.ts",
"utilities.ts",
"binder.ts",
"checker.ts",
"emitter.ts",
"program.ts",
"commandLineParser.ts",
"tsc.ts",
"diagnosticInformationMap.generated.ts"
]
}
compilerOptions プロパティ
compilerOptionsはコンパイラオプションをまとめておける。これはもちろんtscのオプションで、そこにどんなものを書くかはtscのオプションを調べれば分かる。
files プロパティ
filesを指定すれば、コンパイルしたいファイルを指定できる。例えば、core.tsとあれば、ルートディレクトリからのパス、つまり、./core.tsを探す。もし、ディレクトリ内にあるファイルであればdir/core.tsのようにして書けば良い。
パス指定時に使えるワイルドカード
tsconfigで使用するパスにはワイルドカードが使用できる。普通にsome/dir/*のようなものから、**/(サブディレクトリに再帰的にマッチ)のようなワイルドカードも用意されているので、実際に使うときに調べてみる。
コンパイラオプション
module
javascriptはもともとmoduleの概念がなく、ライブラリとしてモジュール的な機能を提供していた。(今はjavascriptにネイティブのモジュール機能がつくようになっているらしい)そのため、色々なモジュールの規格が存在してしまっている。Node.jsではCommonJSが標準になっているので、それを使えばよいが、フロントエンドで使うときはモジュールの仕様を調べて決めてあげないといけない。
None,CommonJS, AMD, System, UMD, ES6, ES2015 , ESNext の中から選択可能。
export & import vs require
tsではexportによってclassやmoduleやfunctionなどを外部ファイルが参照できるようになる。これにもやり方がいくつか並列に存在していてややこしい。
これは、たとえばNode.jsのモジュールの仕様だとか、ES XXXの仕様の違いだとか、色々なりゆうでいくつもモジュールの扱いが存在してしまっているせいでこんなことになっている。(これはコンパイラオプションでも前述したとおり)
いくつか説明を書く。ざっと挙げると、
module.export + requireを使ったNode.js仕様export + importを使ったES6仕様
の2つがあると考えれば良い。他にもあるかもしれないが、とりあえず必要ないので…
Node.jsのモジュールシステム
export側
function someFunc(){...}
module.export = someFunc;
import側
const externalFunc = require('file/path/of/imported');
ES6のモジュールシステム
export側
export function someFunc(){...}
import側
import * as external from 'file/path/of/imported';
import側は基本的な書き方は似ているが色々な書き方がある。