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
側は基本的な書き方は似ているが色々な書き方がある。