6日目:TypeScript の型システムについて


はじめまして、Kodai (Twitter @0918nobita) です。

普段は Web フロントエンドや言語処理系 (レキサ, パーサ, 型検査器) の開発について独学で勉強したり、そこで学んだことを活かして作品を制作したりしています。

型システム と TypeScript

Web フロントエンドとは、要はブラウザ上で動作するアプリの開発を指しているので、必然的に JavaScript というプログラミング言語を扱うことになります。
…ただ、直に JavaScript を記述するのはいろいろな問題があります。
ブラウザ間の互換性の問題は今回は触れないとして、JavaScript という言語自体の問題を考えると、「型システム」の恩恵を受けられないことが挙げられます。
型システムとは、「プログラムの各部分を、それが計算する値の種類に沿って分類することによって、プログラムの振る舞いを保証する構文的手法」です。

この問題を解決する方法に関して、今回は TypeScript を紹介します。これは、マイクロソフトによって開発/メンテナンスされているオープンソースのプログラミング言語で、JavaScript に対して省略可能な静的型付けとクラスベースオブジェクト指向を加えた厳密なスーパーセット (上位互換) になっています。tsc を使用して TypeScript のコードをコンパイルし、JavaScript のコードを出力させてから実行することになります。

TypeScript を実際に使ってみる

TypeScript コードをブラウザ上で記述して、「型」と「コンパイル後に生成される JavaScript コード」を確認できる Playground を用いて、これから示す TypeScript のサンプルコードを実際に変換/実行してみてください。

let foo: number = 2;

 

この TypeScript コードを左側のエディタに入力すると、右側に以下のような JavaScript コードが生成されます。

var foo = 2;

 

let 文は変数宣言を表し、変数名と型注釈を受け取ります。ここでは変数 foo を number 型の注釈付きで宣言し、 2 で初期化しています。生成された JavaScript コードと見比べると、var を用いた変数宣言に変換されています。varlet には、厳密にはスコープの規則に違いがありますが、その違いを考慮した上で生成されています。重要なのは、: number の部分が、変換後のコードに反映されていないことです。JavaScript には、型注釈を付与できる機能は備わっていません。これらのことを考慮に入れた上で、先程の TypeScript コードを以下のように変更してください。

let foo: number = 2;
foo = 'hello';

 

2行目に、変数 foo に文字列リテラル 'hello' を代入する文を追加しました。このとき、右側のエディタでは2行目に foo = 'hello'; と同じ文が追加されますが、左側のエディタで、変数 foo に赤い波線が引かれていると思います。そこにカーソルを重ねると「Type '"hello"' is not assignable to type 'number'」とポップアップで表示されます。TypeScript は JavaScript に変換する前に型検査を行って型の整合性を確認しているため、"hello" 型 (値が "hello" しか取り得ない型) データである "hello"number 型の変数 foo に代入しようとするとエラーが発生します。

関数の型 と 型推論

TypeScript, JavaScript では関数は第一級オブジェクト (他の値と同じように扱える) なので、当然型の概念も存在します。各引数と戻り値について型注釈が記述できます。すべてきちんと指定した例が以下のようになります。

function add(x: number, y: number): string {
  return `${x} + ${y} = ${x + y}`;
}

 

add 関数について、第1引数 x と第2引数 y の型が共に number 型で、返り値の型が string 型であることを示しています。`${x} + ${y} = ${x + y}` の部分では、文字列中に式の評価結果を埋め込んでいます。

先程は全引数と戻り値について型注釈を記述しましたが、実は必ず記述しなければならないわけではなく、型注釈を省略していても可能な限り推論してくれますadd 関数の定義では、戻り値の型注釈である : string の部分を消去しても、TypeScript は return 文で明らかに文字列を返していることを根拠として、 add 関数の戻り値の型が string 型であることを推論します。この機能は型推論と呼ばれています。

型シノニム と ジェネリックな型

以下の TypeScript コードでは、 「tag プロパティの値の型が string 型で、 value プロパティの値の型が number 型であるオブジェクトを示す型」 に、TaggedNumber という別名をつけています。この「別名をつける」機能は型シノニムと呼ばれています。

type TaggedNumber = { tag: string; value: number; };

let tn: TaggedNumber = {
  tag: '生命、宇宙、そして万物についての究極の疑問の答え',
  value: 42
};

console.log(tn.tag);    // tn.tag: string
console.log(tn.value);  // tn.value: number

 

実は、 Generics という機能を利用すると、より抽象的な型に名前を付けて扱えるようになります。例えば、先程の TaggedNumber 型で「value プロパティの値の型が何でも構わない」という風に仕様を変えたいとします。Generics では、この「何でも構わない型」を型引数として扱うことが出来ます。関数でいう仮引数のように、とりあえず名前を付けておいて、実際に利用するときには、そこに当てはめるものを指定する、といった感じです。フワッとした説明はここまでにして、実際に「何でも構わない型」を T 型として定義してみます。

type Tagged<T> = { tag: string; value: T; };

 

型引数を必要とする場合は、型の名前の後ろに型引数名をカンマ区切りで、< > の内側に列挙します。ちなみに TypeScript では配列型は Array<T> と表され、要素の型を型引数で指定できるようになっていることがわかります。Tagged<T> 型で、 tag プロパティの値の型も別途指定できるようにした Detailed<A, B> 型の定義は以下のようになります。

type Detailed<A, B> = { tag: A; value: B; };

 

型引数が存在しない型を具体型、未指定の型引数が存在する型を抽象型と呼びます。抽象型は、そのすべての型引数に対して具体型が指定されることで、全体として具体型に変わります。 let 文や関数定義の型注釈の部分では抽象型ではなく具体型を指定することになっているため、抽象型の変数を宣言したりすることはできません。以下に、先程定義した Tagged<T> 型と Detailed<A, B> 型を用いたサンプルの TypeScript コードを示しておきます。

let taggedNumber: Tagged<number> = {
  tag: 'なんでや',
  value: 334
};

let detailedString: Detailed<string, string> = {
  tag: '橘',
  value: 'ありす'
};

let detailedArray: Detailed<string, Array<number>> = {
  tag: 'これは数値の配列',
  value: [1, 2, 3, 4]
};

 

(雑な) まとめ

ここまで長々と TypeScript について語ってきましたが、いかがでしたか?

Java や C# といった言語に触れたことのある方にとっては、馴染みのある内容も多かったのではないでしょうか。実は TypeScript の型システムは、本質的には Java や C# のそれとは全く異なる仕組みをもっています。今回示した例だけではその違いは現れてきませんでしたが、interfacetypeclass を多用するコードになると、それは顕著に現れてきます。気になる方は、「TypeScript Structural Subtyping」などでググってみてください。これは沼です

今回は紹介しきれませんでしたが、 TypeScript には Conditional Types (extends, infer)、Mapped TypesGeneric rest parameters など、抽象的に型や関数を扱うのに役立つ様々な機能が備わっています。これを活用すると、複雑な挙動をする関数の型を簡潔に定義したり、型の世界で自然数や整数を表現してその演算をコンパイル時に行わせたりすること (いわゆる型レベルプログラミング) が可能になります。厳密な型定義を含む保守性の高いコードを書いたり、高等的な型を使って遊んでみたり… TypeScript の活用の仕方・楽しみ方は様々です。
ぜひ、TypeScript を使って型システムの沼にハマってください。Types and Programming Language って本がおすすめです、絶対に読まないでください

以上、TypeScript の型システムの話でした。

カテゴリー: 2018年度, アドベントカレンダー パーマリンク

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください