zooo-log

読んだものとか、学んだこととか

プログラミングTypeScriptを読んだ(6章・7章)

6章では、TypeScriptの高機能な型が有効的な使い方とともに、紹介されています。

実際のアプリ開発で使う場面を想定したサンプルも載っているため、 実装時に「あ、ここでやったことか」とフックになるような配慮がされているかと思いました。

これらの高度な型機能は、TypeScriptを利用していると、勝手に補完してくれます(例えば、過剰プロパティチェックのような機能)が、 実装中に、なんでこのエラーが出ているんだろう?どう直したら安全だろうか?を考える上で、この章の内容はとても重要だと思いました。 ただ書くだけであれば不要ですが、そんなことをすると後述するアサーション盛りだくさんのコードになり可読性が下がり、コードの品質は上がりませんし、そんなプログラミングは無意味かと思います。

まだまだTypeScriptを学び始めたばかりなので、リファレンスとしてこれからとてもお世話になる章だと思います。

7章では、アプリケーションを実装する上で、必要なエラー処理のパターンを解説してくれる章です。

6章:高度な型

  • 複雑な型(オブジェクトのフィールドが複数あるものや関数の型)は、サブタイプかどうかの判断が難しく、割当可能かどうかを判断するのに変性が鍵となる
  • 変性は、不変性、共変性、反変性、双変性の4つが存在する
    • 不変性:オブジェクトT自体でなければいけないことを示す
    • 共変性:オブジェクトTのサブタイプであるものを示す
    • 反変性:オブジェクトTがサブタイプであるものを示す
    • 双変性:オブジェクトTがサブタイプもしくは、スーパータイプであることを示す
  • 基本的に、TypeScriptは複雑な型はすべて、そのメンバーに関して共変であり、これがサブタイプと判断する条件の一つになる
    • ただし、関数のパラメータの型に対してのみ、反変である(以下に記述)
    • そのため、ある関数Aがある関数Bのサブタイプであると判断するには、以下の条件を満たす必要がある
      • 関数Aが関数Bと同じかそれより少ないパラメータ数を持ち、Aのパラメータの型はBのパラメータのスーパータイプ(:反変性)
      • Aのthisの型が指定されていない、もしくは、Aのthisの型がBのthisの型のスーパータイプ(:反変性)
      • Aの戻り値の型がBの戻り値の型のサブタイプ(:共変性)
  • constアノテーションは、値の制限だけでなく、型の推論を制限させる役割を持つ
  • タグ付き合併型を利用し、合併型に対して、リテラル型を付与し、型の絞り込みを明確にできる
    • 異なるevent型をそれぞれある関数でハンドリングしたい場合に、event型のもつ各プロパティの型が推論できないため、処理内で型を明確にする必要があるが、タグ付き合併型を利用し、タグによって型を推論させることで、個別のプロパティの型推論が不要になる
  • 完全性は、すべてのケースが実装されているか、自動的にTypeScriptがチェックする機能
    • 例えば、ある合併型が戻り値の型であるときに、一部の型だけ戻り値として実装されていると、戻り値に不足があると教えてくれる
      • これによって、switch文のcase文の書き漏れなどが無くなる
  • ルックアップ型は、オブジェクトの一部を型として定義することができるので、ネストされた複雑な型の型定義を容易にしてくれる
    • APIで返されるオブジェクトなど、ネストされている場合に個別なオブジェクトとして扱いたくなるが、それぞれ複雑な型であるため、型定義が冗長になったりするが、ルックアップ型を利用することで簡潔に記述可能
  • keyof演算子は、オブジェクトのすべてのキーを文字列リテラル型の合併として取得できるもの
    • ルックアップ型と併用することで、あるネストされたオブジェクトに対して、型安全にgetter関数が実装可能
  • マップ型とRecord型は、それぞれオブジェクトにおいてどのキーがどの値の型と対応するかを強制させるもの
    • マップ型は、ルックアップ型と組み合わせることで、Recordよりも細かに対応づけを定義できる(基本はこちらを利用すれば良さそう)
    • Record型は、ある型の集合が、別な型の集合のいずれかであるか(N:N)もしくは、ある型の集合がなんの型かを定義(N:1)まで(1:1対応はできない)
  • ユーザ定義ガードは、ある関数がなんの型の戻り値を返すかだけでなく、ある戻り値の型のときに、渡されていた引数の型がなんだったかまでを、型チェッカーに教えるための機能
    • これによって、汎用的な関数を実装する場合に、TypeScriptの型推論がスコープ外でも機能することになり、実装時に型のチェックが不要となる
  • 条件型は、型を三項演算子のように記述し、ある条件に当てはまればAという型、当てはまらなければ、Bという型、というような宣言が可能
    • 条件型は、合併型にも適応可能で、適応時は分配法則に従って型チェックされる
  • 型安全を処理的に自明である際に、TypeScriptの型判定をショートカットしたいようなときには、型アサーションや非nullアサーション、割当アサーションを利用することができる
    • ただし、これはTypeScriptの型推論によるバグの作り込みを回避する機会を、意図的に回避しているため、できるだけ使わないことが望ましい
    • あくまで処理的に自明であるときに限るなど、ルール化は重要
  • TypeScriptは構造的な型システムであるが、名前的型システムを実装することもできる
    • ある型に対してunique symbol型の交差を行うことで、以下の型を別な型と定義できる
    • type CompanyID = string & {readonly brand: unique symbol}type UserID = string & {readonly brand: unique symbol}は別な型と判断される
    • string型だけで定義をすると、TypeScirpt上ではCompanyID型とUserID型は構造的に等しくなるため、実装において制約を課すことはできない

7章:エラー処理

  • 例外を処理する場合、Javaのようなthrowsがないため、関数の戻り値の型に対して、合併型で例外の型を宣言しておく
    • 戻り値を受け取った変数がなんの型なのか、下流のコードで判断つかないため、その判断をする必要がある(判断しない場合エラーになる)
    • この判断をする処理を書くことが、例外処理の記述を促すフックになる
    • 合併型で宣言することで、関数自体の実装時には、TypeScriptの完全性によってすべての戻り値の型が返されているかをチェックされている
  • Option型という関数型言語などに由来する仕組みを利用して、値があるかないかわからない処理に対応するケース
    • JavaScriptには標準的に実装されていないため、独自実装もしくはライブラリを利用することになる
    • 値を返すのではなく、値やメソッドが含まれたコンテナと呼ばれるオブジェクトを返すことが基本となる
      • この本では、配列をコンテナとして使うケースやクラスを定義することで実現している
    • Option型ではあくまで、「失敗したことを知らせるだけ」であり、なぜエラーが起きたのかといった情報は得られない

個人的にエラー処理は、例外を用いて、「なぜ起きたのか」や「安全に処理を継続させる」、といったことに重きを置くべきと思っているので、Option型は用途が違うため、使うことはなさそうです。 関数型言語ではこういうのを使ってエラー処理を実装することもある、ということを知れたのは新たな発見だったので、そこは面白かったです。