【読書メモ】初めてのTypeScript - 第I部 TypeScript の概念

はじめに

だいぶ前から TypeScript を使い始めていますが、理解を深めるために O'Reilly の「初めてのTypeScript」を読んでみることにしました。

この記事では、「第Ⅰ部 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 に対する誤解

  • 悪いコードの改善策である
    • TypeScriptJavaScript を構造化するために役立ちますが、堅安全性を強制すること以外には、どのような構造にすべきかといういかなる意見も強制しません。
  • (大部分が)JavaScript の拡張である
    • TypeScriptJavaScript の動作を一切変えようとしません。
    • 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 チェックが有効ではないと、作成するコードに、予期しない nullundefined によるエラーの危険があるかどうかを知るのが難しくなります。

一般的に 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;
// };