こんにちは@binarytaです。
そろそろ新卒が入ってくる時期なので優しめの記事を書いていきます。
はじめに
ここ数年でGithubの中で最も使用されるようになった言語として名を馳せているJavaScriptですが、まだ当言語を好きになれない、苦手だというエンジニアは多いように感じます。
実際、JavaScript V8エンジンにレキシカルスコープやアロー関数など、es6による多彩な言語機能が搭載される前まではかなり癖のある言語だったというのも理解できます。
またプロトタイプチェーンによるclass定義をしていたes6以前の言語実装には曲者感が漂います。
本記事で言いたいのはモダンJavaScriptのことは一旦忘れて、TypeScriptという最強の鎧を纏ったJavaScript(の互換言語)がどれほど便利かつWeb Applicationのフロントエンド開発で有効になるのかということです。
TypeScriptを初めて知る方には、モダンなJavaScriptを使っていた頃には感じられなかった素晴らしい体感を得られることでしょう。
Let's learn TypeScript !
Introduction
この節ではTypeScriptの型を取り扱います。
退屈な人はずっと下の方へスクロールしてください。
まずモダンなJavaScriptとの大きな違いの1つは型システムが提供されることであり、これにより世のエンジニアの精神衛生面を大幅に和らげてくれます。
TypeScriptを導入していない場合、ある機能を実装した際に多くの動作をブラウザ上で実際に動作するかどうかを確認するという忍耐強く日の暮れるほど面倒な作業が待ち受けています。
(少々言い過ぎかもしれません)
それではTypeScriptが提供するプリミティブ型をザックリ見ていくとしましょう。
Boolean, Number, String
/* Boolean */ let isDone: boolean = false; isDone = 'true string'; // => compile error /* Number */ let decimal: number = 6; let hex: number = 0xf00d; let binary: number = 0b1010; let octal: number = 0o744; decimal = '6'; // => compile error /* String */ let color: string = "blue"; color = 1; // => compile error
宣言修飾子(const, let, var)の後に続けて変数名: 型
を記述し型を明示します。
(constの場合は初期化必須)
異なる型を再代入するともちろんエラーを吐きます。
チーム開発を行う際には本当に精神衛生上よろしいのです。
number型
に関してはバイト範囲を与える修飾子までは備わっていませんが、モダンJavaScriptに比べれば格段に安心できます。
次にプリミティブ型以外のnull / undefinedとオブジェクト型(参照型)についても見ていきましょう。
Null, Undefined
// Not much else we can assign to these variables! let u: undefined = undefined; let n: null = null;
Array, Tuple, Enum, Any, Void
/* Array */ let list: number[] = [1, 2, 3]; let list: Array<number> = [1, 2, 3]; // same as above /* Tuple */ let x: [string, number]; x = ["hello", 10]; x = [10, "hello"]; // compile error /* Enum */ enum Color {Red, Green, Blue} let c: Color = Color.Green; /* Any */ let unknow: any = 4; unknow = "maybe a string instead"; unknow = false; let list: any[] = [1, true, "free"]; /* Void */ function warnUser(): void { console.log("This is void function"); }
多くの言語に存在する配列を表すArray型([])も存在しますし、Tupleによりインデックス数が固定の表現を表す型も存在しています。
さらにはEnum(列挙型)により列挙定数なども表現しやすくなっています。
TypeScriptを使用する多くのエンジニアはこの辺はよく使うのではないでしょうか?
TypeScriptには他の言語には無い面白い表現(型)が1つあります。
Never
/* Never */ function error(message: string): never { throw new Error(message); } function fail() { return error("Something failed"); } function infiniteLoop(): never { while (true) { } }
Neverという型は変数定義時に使うと何も代入できない (代入すると例外を吐く) ため到達不可能であることの明示になります。
また上記のようにエラーハンドリング処理を行う関数の定義時に使用すると例外が吐かれて処理が中断されることの明示にもなります。
僕自身、実際にアプリケーションの実装で使用したことはありませんが、使ってみるのもありでしょう。
この節は以上です。
次にclassについて見ていきますが、もちろんes6が提供するclassとの違いに焦点を当てていきます。
Class
es6が提供するclassはインスタンス変数やインスタンスメソッドへの参照に対するスコープにまでは気を配ってくれません。
癖の強いやり方は存在しますが、それは実装の複雑さが増すのでなかなか手を出したくないアプローチです。
es6のclassはアクセス修飾子が提供されていないため常に開放的で不安になってしまいます。
常に開放的であることは多大なデバッグコストを費やすことにつながるので一大事です。
TypeScriptはes6のclass機能に対して、アクセス修飾子としてpublic
, private
, protected
という強靭な鎧を纏わせてくれます。
(大げさに言いましたが、多くの言語には当たり前のように存在している概念です)
public
修飾子は省略可能です。
class Hello { public a: string; private b: string; constructor(a, b) { this.a = a; this.b = b; } access(): void { this.a; // OK this.b; // OK } } const obj = new Hello('a', 'b'); obj.access(); obj.a; // OK obj.b; // Error
なんということでしょう。
Hello classにprivate
修飾子で定義したb
というインスタンス変数が外部からの参照に対してコンパイルエラーを吐いてくれました。
期待通りなのですが、モダンなままでは味わえない快感と安心です。
classに対する修飾子についても紹介します。
abstract class Animal { abstract makeSound(): void; move(): void { console.log("roaming the earth..."); } }
これは抽象クラスですね。
もちろん多くの言語に存在するような抽象クラスと同様に動作します。
抽象クラスはnew
キーワードによるインスタンス生成を抑制してくれます。
インスタンス生成が出来ないので抽象クラスを継承して使い、抽象クラスは継承するクラスのベースとなる実装を定義します。
開発の設計意図の明示にもなるので僕もたまに使うことがあります。
クラスの紹介は以上。
Interfaces
interfaceはTypeScriptと共にフレームワークを使用する際などには多用します。
ReactやHyperapp等のフレームワークでpropsオブジェクトをinterface型の引数オブジェクトとして受け取ることがよくあります。
また、ライブラリを使用する際に型定義ファイル(~.d.ts)を覗いてみると頻繁に使用されていて、ライブラリを使う側はそのinterfaceに注意を払う必要があります。
以下は僕がHyperappをTypeScriptと共に使用してみたときの実例です。
Hyperapp x TypeScriptに興味がある方はその記事を別のエントリとして書いたので是非閲覧ください。
import { ActionsType, ActionResult } from "hyperapp" import { State } from "./state" export interface Actions { down: (value: number) => (state: State) => ActionResult<State>; up: (value: number) => (state: State) => ActionResult<State>; } export const actions: ActionsType<State, Actions> = { down: (value: number) => (state) => { return { count: state.count - value } }, up: (value: number) => (state) => { return { count: state.count + value } } }
僕の例だけでは不充分な場合は以降の簡単な使用例を見てみてください。
interfaceはObjectのプロパティに対する型定義を提供してくれる素晴らしい機能です。
TypeScript入門中の時はObjectの型定義をする際にどのように定義したらいいのか戸惑うことがありました。
最初のうちは以下のように少し癖のある書き方で定義していました。
let obj: { [s: string]: string; }; obj["hello"] = "world"
しかし、これだとTypeScriptのコンパイラーは何故かObjectのキーが文字列以外の時でも型を許容しますし、sというkeyでなくても代入可能なのです。
僕自身の見解ではありますがそもそもObjectのkeyは最初からわかっていることの方が多いため、key名に対するvalueの型定義をする方が理に叶っています。
ということでinterfaceの簡単な例をいくつか記します。
上述したObjectのkey名とvalueの型定義に関するinterfaceです。
以下の例では、printLabel関数はlabel
というkeyを持ちvalueがstring
型であるObjectを期待します。
interface LabelledValue { label: string; } function printLabel(labelledObj: LabelledValue) { console.log(labelledObj.label); } let myObj = {size: 10, label: "Size 10 Object"}; printLabel(myObj);
interfaceはclassに対してもアプローチが効きます。
以下の例はClock classはcurrentTime
というインスタンス変数とsetTime
というメソッドを持つことを保証し、それぞれDate型の値を持つこと、Date型の値を返却することをコンパイラがチェックします。
interface ClockInterface { currentTime: Date; setTime(d: Date); } class Clock implements ClockInterface { currentTime: Date; setTime(d: Date) { this.currentTime = d; } constructor(h: number, m: number) { } }
interfaceの概念はJavaをやっている方ならすんなり頭に入ってくるかと思います。
TypeScriptは全体的にJavaに似ています。
Decorator
DecoratorはAngularを使った開発をする方はよく目にするはずです。
Vue.jsでもTypeScriptで開発するためにnode_moduleの vue-class-component
という公式でサポートされたライブラリを使用するとDecoratorが登場してきます。
Decoratorは非常に強力ですが、おそらく使用頻度はそこまで高くありません。
試しに一つ@debug
というメソッドデコレータを作ってみることにしましょう。
このメソッドデコレータはメソッドの引数で受け取った変数全てをconsole.logとして標準出力してくれるというものにしましょう。
/* method decorator function */ const debug = () => { return (target, propertyKey: string, descriptor: PropertyDescriptor) => { const method = descriptor.value; descriptor.value = (...values: string[]) => { values.forEach(value => { console.log(value); }); return method.apply(); } } } class MyClass { @debug() method(str: string, num: number) { console.log('method is called'); } } const obj = new MyClass(); obj.method('Hello', 100); // => Hello // => 100 // method is called
このようにメソッドに対してフックを噛ませることも可能ですし、アイデア次第では非常に柔軟です。
Decoratorに関しては説明が複雑なので是非TypeScriptのドキュメントを参照してみてください。
TypeScriptによるWeb開発の実際
TypeScriptを単体で使用するというようりもReact
やAngular
, Vue.js
などのフレームワークと組み合わせて使うことの方が現状としては多いでしょう。
Viewを管理するフレームワーク以外にもRxJxやNativeScriptなどの有名フレームワークもTypeScriptをサポートしています。
しかし、TypeScriptはそれ単体でも効力抜群です。
DOMの全ての要素の型もTypeScriptが用意してくれているので、型を明示して要素へアクセスすると要素特有のプロパティへのアクセスもコンパイル時に評価してくれます。
例えば、以下のinput
タグのDOMに対してid属性を参照してFile APIを使用したいとしましょう。
<input id='download'></input>
そのような場合、TypeScriptのジェネリクス演算子を用いて以下のようにFile APIのプロパティへのアクセスが可能となります。
// ok (<HTMLInputElement>document.getElementById("download")).files; // error document.getElementById("download").files; // -> Property 'files' does not exist on type 'HTMLElement'.
通常のDOM参照をした場合、HTMLElement
という型が適用されるためキャストなりジェネリクス演算子なり、適宜必要な型に変換してあげる必要があります。
DOM要素に対してここまで厳格に型チェックをしなくてもいいとも思ったりもしますが、せっかくなので積極的に使ってみるのも良いと思います。
僕自身、DOM要素の型キャストが面倒な時は以下のようにanyで定義することもあります。
(<any>document.getElementById("download")).files; /* same as above */ (document.getElementById("download") as any).files;
(DOM要素の型: 参考リンク)
https://github.com/Microsoft/TypeScript/blob/v2.7.2/src/lib/dom.generated.d.ts
Flowとの違い
型を提供してくれる互換言語としてもう一つFacebook製のFlowというものがあります。
これに関しては僕の記事で軽く紹介しているので是非ご覧ください。
最後に
鎧は重すぎると俊敏性を失いますが圧倒的な防御力が備わります。
あらゆる型に対して厳格すぎるのは開発効率の低下を招くように思いがちかもしれませんが、安全第一な長期開発には非常に有用です。
逆にスタートアップのように欲しい機能を即座に求めるような開発には不向きかと思われるかもしれませんが、実際のところ開発速度に大きなコストがあるとは僕自身は思いません。
FlowやTypeScriptをまだ知らない人に関しては多少の学習コストが必要かもしれませんが、習得後には多大に恩恵を受けられることでしょう。
是非、型と共にJavaScriptを!
最後までご閲読いただきありがとうございました。
===== 間違いに対する指摘や記事に関する感想をコメントを頂けると幸いです =====