TypeScriptは過去最高のプログラミング言語だと思う
書きかけだったのになんか見られてるらしいので書き足すかも?keroxp.icon2019/1/28 hr.icon
かなり寝かしていたのでそろそろ書いてみるkeroxp.icon2018/7/14
昨年の9月くらいから仕事でNode.jsとReactを使ってWebアプリを作り始めたとき、最初は普通にbabelとbrowserifyを使って開発していた しかしまずbabelの設定ファイルで消耗し、すぐに開発を始めることができなかった
そして開発をしていくうちに3日くらいでもうTypeError: xxxx is not functionみたいなエラーが出まくるようになった
jsだと関数の引数や返り値の型がわからず、静的解析もできず、エディタのコード補完も効かず、暗中模索でデバッグする羽目になった
それでやっぱり1ファイルを超えるプログラムには型が必要だと悟り、型付JSを探し始めた
実を言うと3年ほど前はHaxeを使ってWebアプリを開発していたが、コミュニティの冷め具合に絶望してやめてしまった
そこでまず試してみたのがFacebookのFlowだった
しかしすぐにFlowが求めているものでないことが分かった
理由としては……なんかJSじゃない感じだったから
あとはエディタの補完が全然ちゃんと動かなかったこともある
それでTypeScriptを使ってみたら驚くほど簡単に導入できた
まず前提として、TypeScriptは現在もECMA Scriptのスーパセットとして作られていることが重要だった
これがどういうことかというと、JSはTSに含まれるということだ
つまりJSで書いたものの拡張子を.tsに変えればすぐにコンパイルできる
allowJSとかのオプションを付ければjsとtsを混在させることもできる
つまりTSはJavaScriptの機能を100%使える上で、TypeScriptの機能を使える
僕はJavaScript自体は相当に好きな言語なのでこれは大きかった
ということで実際によいポイントを列挙していく
tsc
tscはTypeScriptのコンパイラだが、この設定がめちゃくちゃ楽である
Babelのような謎設定やpluginを入れる必要なく、素のtscでtsをjsにコンパイルできる
tsconfig.jsonを作る必要もあるのだが、これは最低限のことだけ書いておけばいい
tscはなんとデフォルトでjsxのコンパイラも含んでいて、ReactのコンポーネントもJSにコンパイルできる
これが最高だった
型システム
TypeScriptの型システムはとても良くできている
本当に過去の静的型付き言語を置き去りにするレベルの進化をしている
tsの型システムのすごいところはいくつもあるのだが、一番画期的だったのは、JSのオブジェクトを型として定義できるようにしたところだと思う
今まで型というのは暗黙的にクラスのことであり、インターフェイスも結局クラスに実装する必要があった
しかしtsにおける型は、type宣言子を使わなくても型として使うことができる
これがどういうことかというと、通常のプログラミング言語で型を使う場合はこういった記述になると思う
code:ts
type SomeParams = {
id: string,
}
type SomeResult = {
id: string
name: string
}
function some(params: SomeParams) : SomeResult {
...
}
まず型定義があって、シグネチャは定義済みの型を適用して解決するという感じ
しかし、TypeScriptは型をインラインに定義することができる
わかりやすく書くとこう
code:ts
function some({id}: {id: string}): {
id: string,
name: string
} {
...
}
引数の型も、返り値の型もその場で宣言してちゃんと解決できる
これすごくないですか
古の言語ジャバではこれを書くために少なくともファイルが3つは必要だった
ジャバは1ファイル1クラス/インターフェイスだからね
更にいうと、TypeScriptの型システムは、明示的にその型を実装していなくても解決される
エディタの補完
TypeScriptは、その(ちゃんと解決するのが)複雑な型システムにもかかわらず、エディタとの相性が最高にいい
IntelliJ系は他のどのエディタも最高クラスに使いやすく、最近はIntelliJでしか開発してない
無料のものだとVSCodeが一番有名かも
開発元が同じだしね
ただインテリセンスの充実度でいうとIntelliJに比べると機能が劣るのであまり使っていない
TypeGuard
TypeScriptにはTypeGuardという機能がある
これはTypeScriptが生み出した数ある革命の一つだとおもう
まず根本的な問題として、JavaScriptをベースとしたTypeScriptには厳密に型を解決しづらいという難点がある
これは、TypeScriptの型は単なる静的解析のためのものであり、ランタイムでは型情報がオミットされ、一切利用されないという方針によるものだ
なぜならTypeScriptはJavaScriptへのトランスパイルを挟まないと実行することはできず、JSになった時点でTypeScriptの静的型情報は消えざるを得ない
interfaceやtypeといった型定義はclassと違い、ランタイムにその型のオブジェクトであると判別することができない
classのインスタンスであれば、instanceof演算子で動的に解決することも可能
例えばこういう型があったとして
code:ts
type User = {
id: string,
name: string
}
type Dog = {
id: string,
dogName: string
}
こういう多態的な関数があったとする
code:ts
//人間か犬の名前を返す
function getName(userOrDog: User | Dog): string {
...
}
ちなみにこの宣言方法はTypeScriptのUnion Typeというもので、型の論理和である
この関数の引数の型は実行時まで分からないが、実行時にはただのJSオブジェクトしか渡ってこない
こういう状況でどうやってname/dogNameを静的に解決するか?
ちなみに、こういう書き方はできない
code:ts
function getName(userOrDog: User | Dog): string {
return userOrDog.name || userOrDog.dogName
}
コンパイルエラーになる
code:log
hoge.ts:10:21 - error TS2339: Property 'name' does not exist on type 'User | Dog'.
Property 'name' does not exist on type 'Dog'.
10 return userOrDog.name || userOrDog.dogName
~~~~
hoge.ts:10:39 - error TS2339: Property 'dogName' does not exist on type 'User | Dog'.
Property 'dogName' does not exist on type 'User'.
10 return userOrDog.name || userOrDog.dogName
~~~~~~~
静的解析の時点では引数の型にはname、もしくはdogNameが確実に存在するとは判断されない
ちなみにこういうのは解決される
code:ts
function getId(userOrDog: User | Dog): string {
return userOrDog.id
}
Union Typeは、集合論的にIntersectionな型情報は解決してくれる
code:log
User | Dog
( name ( id ) dogName )
こういった曖昧な型を解決するための機能がTypeGuardである
TypeGuardは、自分で定義した(クラス以外の)型については基本的に自分で型を解決する手段を用意するという方針だ
UserとDogを判別するためのTypeGuardはこう記述する
code:ts
function isUser(x): x is User {
return x.hasOwnProperty("id") && x.hasOwnProperty("name")
}
function isDog(x): x is Dog {
return x.hasOwnProperty("id") && x.hasOwnProperty("dogName")
}
関数の返り値のis XXXというのがTypeGuardの書き方で、この関数は引数xがUser/Dogかどうかのbooleanを返すTypeGuardであるという意味を持つ
これらを用いて先程の関数を書き直すと、
code:ts
function getName(userOrDog: User | Dog): string {
if (isUser(userOrDog)) return userOrDog.name;
if (isDog(userOrDog)) return userOrDog.dogName;
}
こういう記述になる
TypeScriptの型にはスコープがあり、TypeGuardで守られたスコープの中ではその変数はその型であると保証される
大事なことなのでもう一度言います
TypeScriptの型にはスコープがあります
tsのany型やUnionType型はTypeGuardによって実際の型(のようなもの)として参照できるようになる
ちなみにtypeof、instanceofも同様のTypeGuardとして働く
これは非常に便利である
本来上記のようなUnionTypeを持った関数を作るべきではないのだが、どーーーーーーーーーーしても止むにやまれない事情においてそういうことをせざるを得ない時に、一番ラクに解決できるのがTypeGuardである
静的型付け言語のKotlinとかには同様の理由でEither<L,R>とかTriple<L,C,R>みたいな型があるのだが、そういう面倒なことをせずにヒューリスティックに解決できるところがすごいと思っている
何故かと言うと、TypeGuardは型情報を使わずに型を解決するという一見無意味なことをやっているわけだが、そういうゆるい解決方法が結局柔軟に働く場面が多い
こういうサボり方もできるので
code:ts
function isDog(x): x is Dog {
return !!x.dogName
}
また、TypeScriptは型スコープも論理解決してくれる
上記の関数は、実はこうもかける
code:ts
function getName(userOrDog: User | Dog): string {
return isUser(userOrDog) ? userOrDog.name: userOrDog.dogName
}
三項演算子の一番右、すなわち userOfrDog ≠ User ⇔ userOrDog = Dogが確定したスコープでは、userOrDogはDogとして解決されている
これやばすぎない?
引数に複数の型の混合型を渡すのがどれだけ大変なことなのかというのは、他の静的型付け言語を知っている人ならわかると思う
実際の開発時は、クラスを型として使うことはまずなく、ファクトリとtypeやinterfaceを使うわけなのでこういった解決方法はすごい革命だと思う
コンパイルが速い
tsはコンパイルがとにかく速い
コンパイルと言うかトランスパイルだから当然なのだけど、これだけ複雑な型システムを解決してjsにする時間が速すぎる
ナントカpackとかよりも速いと思う