はじめに
だいぶ前から TypeScript
を使い始めていますが、理解を深めるために O'Reilly の「初めてのTypeScript」を読んでみることにしました。
この記事では、「第Ⅰ部 TypeScript の概念」についての内容をまとめていきます。
1章 JavaScript から TypeScript へ
JavaScript の欠陥
- コストのかかる自由
JavaScript
の重要な特徴の1つは、コードをどのように組み立てるかに関して、実質的に何の制限も設けていないことです。- ファイルの数が多くなるにつれて、このような自由が有害であることが明らかになり、コードを安全に実行したい場合には本当に苦痛となります。
- 規律のないドキュメンテーション
JavaScript
の言語仕様には、関数のパラメーターや戻り値、変数、その他の構成体がどのようなものかを記述するための正式な取り決めは何もありません。
- 開発者用ツールの貧弱さ
JavaScript
には、型を識別するための組み込み方法はなく、コードベースへの大規模な変更を自動化したり、コードベースを正しく理解したりするのが難しい場合があります。
TypeScript とは何か
- プログラミング言語
- 既存の全ての
JavaScript
の構文に加え、型を定義したり使用したりするためのTypeScript
独自の新しい構文を含む言語
- 既存の全ての
- 型チェッカー
JavaScript
および(または)TypeScript
で書かれたファイル一式を受け取り、そこで定義された全ての構成体(変数、関数など)を理解し、何かが間違って設定されていると判断した場合にユーザーに知らせるプログラム
- コンパイラー
- 型チェッカーを実行し、問題があればそれを報告し、相当する
JavaScript
コードを出力するプログラム
- 型チェッカーを実行し、問題があればそれを報告し、相当する
- 言語サービス
- 型チェッカーを使って、有益なユーティリティを開発者に提供する方法を、VS Code などのエディターに伝えるプログラム
TypeScript の利点
- 制限による自由
TypeScript
では、パラメーターや変数にどのような型の値を割り当てられるかを指定できます。- 指定した方法でのみコードを使用できるように制限することで、コード内のある領域での変更が、それを使用する他の領域のコードを壊すことがないという確信が得られます。
- 正確なドキュメンテーション
TypeScript
に備わった、オブジェクトの「形状」を記述するための構文は、オブジェクトがどのようなものであるべきかを表現する素晴らしい強制的なシステムを提供します。
- 強力な開発者用ツール
TypeScript
の型付けによって、VS Code などのエディターは、ユーザーのコードに関してより深い洞察を得られます。- そのような洞察を用いて、エディターはユーザーの入力時に気の利いた提案を示すことができます。
TypeScript に対する誤解
- 悪いコードの改善策である
TypeScript
はJavaScript
を構造化するために役立ちますが、堅安全性を強制すること以外には、どのような構造にすべきかといういかなる意見も強制しません。
- (大部分が)JavaScript の拡張である
TypeScript
はJavaScript
の動作を一切変えようとしません。TypeScript
の作成者たちは、JavaScript
の機能への追加やそれとの衝突を引き起こすような、新しいコード機能の導入を避けようと懸命に努力してきました。
JavaScript
より遅い- 進化が終わった
Web
はまだまだ進化を終えていませんし、TypeScript
もそうです。TypeScript
言語は、絶え間なく変わる Web コミュニティのニーズに合わせるために、頻繁にバグ修正や機能追加が行われています。
2章 型システム
「型」とは
型 とは、JavaScript
の値の 形状 がどのようなものであるかを説明するものです。「形状」とは、ある値にどのようなプロパティやメソッドが存在しているか、また組み込みの typeof
演算子がそれをどのように表現するか、といったことを指します。
TypeScript
での最も基本的な型は、JavaScript
の7種類の基本的なプリミティブ型です。
- null
- undefined
- boolean
- string
- number
- bigint
- symbol
「型システム」とは
型システム とは、プログラム内の構成体がどのような型を持ち得るかをプログラミング言語が理解する方法を定めた、一連のルールのことです。
TypeScript
の型システムは、根本的には次のように機能します。
- コードを読み込み、存在している全ての型と値を理解する
- それぞれの値について、それが最初に現れた際の宣言から、どのような型を持ち得るのかを調べる
- それぞれの値について、それがコード内でその後どのように使われているかをすべて調べる
- 値の使い方がその型とマッチしていなければ、ユーザーにエラーを報告する
型エラーと構文エラーの違い
TypeScript
を書いているとき最も頻繁に遭遇する「エラー」は、構文エラー と 型エラー です。
構文エラーは、TypeScript
が、コードとして理解できない、間違った構文を検出した時に発生します。このエラーは、TypeScript
がソースファイルから JavaScript
を正常に生成することをブロック(抑制)します。
let let pokemon;
// ~~~~~~~
// ',' expected.
型エラーは、構文は有効でも、プログラムの型に関するエラーが TypeScript
の型チェッカーに検出された時に発生します。型エラーは、TypeScript
の構文から JavaScript
への変換をブロックしません。ただし、それらは多くの場合、コードを実行するとクラッシュするか、または期待どおりに動作しないことを示します。
console.blub('pokemon');
// ~~~~
// Property 'blub' does not exist on type 'Console'.
型アノテーション
初期化時に型が推論されない変数は、進化する any と呼ばれるものになります。つまり、TypeScript
は何らかの特定の型を強制するのではなく、新しく値が割り当てられるたびに、その変数の型についての理解を進化させるということです。進化する any 型の変数を許容すること、および一般的に any 型を使用することは、TypeScript
の型チェックの目的を部分的に損なってしまいます。
TypeScript
には、変数に初期値を割り当てることなく変数の型を宣言するための、型アノテーション と呼ばれる構文が用意されています。
let pokemon: string;
pokemon = 'Pikachu';
3章 合併型とリテラル型
合併型
合併型 は、値がどの型であるかは正確にわからないけれど、2つ以上の選択肢のうちの1つであることはわかっているという状況に対処できる、素晴らしい概念です。
const pokemon: string | undefined = Math.random() > 0.5 ? 'Pikachu' : undefined;
ある値が合併型であることがわかっている場合、TypeScript
は、その合併型に含まれる全ての型に存在するメンバープロパティへのアクセスだけを許可します。そうでないプロパティにアクセスしようとすると、型チェックのエラーが発生します。
const pokemon: string | number = Math.random() > 0.5 ? 'Pikachu' : 42;
pokemon.toString(); // OK
pokemon.toUpperCase(); // Property 'toUpperCase' does not exist on type 'number'.
pokemon.toFixed(); // Property 'toFixed' does not exist on type 'string'.
型の絞り込み
型の絞り込み とは、ある値が、定義された型、宣言された型、あるいは以前に推論された型よりも限定的な型であることを、TypeScript
にコードで示すことです。型の絞り込みのために利用できる論理チェックのことを、型ガード と呼びます。
- 割り当てによる絞り込み
let pokemon: string | number;
pokemon = 'Pikachu';
pokemon.toUpperCase(); // OK
pokemon.toFixed(); // Property 'toFixed' does not exist on type 'string'.
- 条件チェック
const pokemon: string | number = Math.random() > 0.5 ? 'Pikachu' : 42;
if (pokemon === 'Pikachu') {
pokemon.toUpperCase(); // OK
}
pokemon.toUpperCase(); // Property 'toUpperCase' does not exist on type 'number'.
- typeof チェック
const pokemon: string | number = Math.random() > 0.5 ? 'Pikachu' : 42;
if (typeof pokemon === 'string') {
pokemon.toUpperCase(); // OK
} else {
pokemon.toFixed(); // OK
}
リテラル型
リテラル型 は、プリミティブ型の何らかの値としてではなく、プリミティブ型の特定の値として理解される型です。
const pokemon = 'Pikachu';
// ~~~~~~~
// const pokemon: "Pikachu"
厳格な null チェック
TypeScript
コンパイラーのオプション strictNullChecks
は、厳格な null
チェックを有効にするかどうかを切り替えるためのものです。厳格な null
チェックが有効ではないと、作成するコードに、予期しない null
や undefined
によるエラーの危険があるかどうかを知るのが難しくなります。
一般的に TypeScript
のベストプラクティスは、厳格な null
チェックを有効にすることです。
const pokemon: string | undefined = Math.random() > 0.5 ? 'Pikachu' : undefined;
if (pokemon) {
pokemon.toUpperCase(); // OK
}
pokemon.toUpperCase(); // 'pokemon' is possibly 'undefined'.
型エイリアス
TypeScript
では、再利用される型に、より簡単な名前を割り当てるための 型エイリアス が用意されています。
type Pokemon = 'Pikachu' | 'Charmander' | 'Bulbasaur' | 'Squirtle';
const pokemon1: Pokemon = 'Pikachu';
const pokemon2: Pokemon = 'Charmander';
const pokemon3: Pokemon = 'Bulbasaur';
const pokemon4: Pokemon = 'Squirtle';
4章 オブジェクト
オブジェクト型
{...}
の構文を使ってオブジェクトリテラルを作成すると、TypeScript
はそのプロパティから、新しいオブジェクト型(型の形状)を推論します。そのオブジェクト型は、オブジェクトの値と同じプロパティ名およびプリミティブ型を持つことになります。
const pokemon = {
name: 'Pikachu',
level: 50,
};
pokemon.name; // 型: string
pokemon.level; // 型: number
pokemon.hp; // Property 'hp' does not exist on type '{ name: string; level: number; }'.
オブジェクト型は、プロパティの名前と型を指定することで、手動で定義することもできます。
const pokemon: {
name: string;
level: number;
} = {
name: 'Pikachu',
level: 50,
};
型エイリアスを使用して、オブジェクト型を再利用することもできます。
type Pokemon = {
name: string;
level: number;
};
const pokemon: Pokemon = {
name: 'Pikachu',
level: 50,
};
構造的型付け
TypeScript
の型システムは、構造的型付け を採用します。これは、たまたまある型を満たす任意の値を、その型の値として使えることを意味します。
type Pokemon = {
name: string;
level: number;
};
type Trainer = {
name: string;
age: number;
};
const someObject = {
name: 'Ash',
level: 10,
age: 10,
};
const pokemon: Pokemon = someObject; // OK
const trainer: Trainer = someObject; // OK
オブジェクト型のプロパティは、オブジェクト内で全て必須である必要はありません。プロパティの型アノテーションの中で :
の前に ?
を含めることで、それがオプションプロパティ(省略可能なプロパティ)であることを表現できます。
type Pokemon = {
name: string;
level: number;
hp?: number;
};
const pokemon: Pokemon = {
name: 'Pikachu',
level: 50,
};
オプションプロパティと、undefined
を含む合併型を持つプロパティとは、違いがあります。オプションプロパティは存在しないことが許されます。一方、必須かつ| undefined
と宣言されたプロパティは、たとえ値が undefined
であっても、存在していなければなりません。
type Pokemon = {
name: string;
level: number;
hp: number | undefined;
};
const pokemon1: Pokemon = { // Property 'hp' is missing in type '{ name: string; level: number; }' but required in type 'Pokemon'.
name: 'Pikachu',
level: 50,
};
オブジェクト型の合併型
ある値がオブジェクト型の合併型である場合、TypeScript
の型システムは、それらの全てのオブジェクト型に存在しているプロパティへのアクセスだけを許可します。
type Pokemon = {
name: string;
level: number;
};
type Trainer = {
name: string;
age: number;
};
type Entity = Pokemon | Trainer;
const entity1: Entity = Math.random() > 0.5
? { name: 'Pikachu', level: 50 }
: { name: 'Ash', age: 10 };
entity1.name; // OK
entity1.level; // Property 'level' does not exist on type 'Trainer'.
コード内でオブジェクト形状をチェックすれば、TypeScript
の型の絞り込みがオブジェクトに対して適用されます。
ただし、if (entity1.level)
のようなチェックは許可されないことに注意してください。
if ('level' in entity1) {
entity1.level; // OK
} else {
entity1.age; // OK
}
if (entity1.level) { // Property 'level' does not exist on type 'Trainer'.
entity1.level;
}
オブジェクトがどのような形状であるかを示すプロパティを持つ合併型は、タグ付き合併型 と呼ばれ、オブジェクトの形状を示すプロパティは タグ と呼ばれます。
type Pokemon = {
type: 'pokemon';
name: string;
level: number;
};
type Trainer = {
type: 'trainer';
name: string;
age: number;
};
type Entity = Pokemon | Trainer;
const entity1: Entity = Math.random() > 0.5
? { type: 'pokemon', name: 'Pikachu', level: 50 }
: { type: 'trainer', name: 'Ash', age: 10 };
if (entity1.type === 'pokemon') {
entity1.level; // OK
} else {
entity1.age; // OK
}
交差型
交差型 は、既存の複数のオブジェクト型を組み合わせた新しい型を作成するために、主にオブジェクト型エイリアスと一緒に使われます。
type ArtWork = {
genre: string;
name: string;
};
type Writing = {
pages: number;
name: string;
};
type Book = ArtWork & Writing;
// 下記型と同じ
// type Book = {
// genre: string;
// name: string;
// pages: number;
// };