Open27

プロを目指す人のためのTypeScript入門 学習メモ #ブルーベリー本

k-bindk-bind

3.1.6 オブジェクトはいつ"同じ"なのか

スプレッド構文のコピーは変数間でオブジェクトが共有されるので注意。
またネストしたオブジェクトも含め全てコピーする標準的な方法は存在しない。

k-bindk-bind

3.2.4 interface宣言でオブジェクト型を宣言する

interface宣言はtype文で代用可能であり、ほとんどの局面ではtype文が使われる。
そのためtype文のみ使うという流儀も存在する。
type文では任意の方に対して別名で宣言可能。
一方、interface宣言で扱えるのはオブジェクト型だけ。

k-bindk-bind

3.3.3 余剰プロパティに対する型エラーについて

余剰プロパティに関するエラーは、型安全性に関するエラーとは関係がない。
型安全性な状態でも余剰プロパティエラーは発生しうる。
(部分型関係はプロパティ包含関係の条件が満たされていればOKなので)
余剰プロパティエラーは「このプロパティはアクセスできないから存在しても意味ないよ」という旨のエラー。

k-bindk-bind

3.6.4 分割代入のデフォルト値

デフォルト値はundefinedのみに対して適用される。

type Obj = { foo?: number };
const obj1: Obj = {};
const obj2: Obj = { foo: -1234 };

const { foo = 500 } = obj1;
const { foo: bar = 500 } = obj2;

この場合、fooのみ500が代入される(barには-1234が代入される)
理由はobj1.fooundefinedであるため。
対象プロパティの値がundefinedならデフォルト値、既に値が入っているなら既定値が入ると覚えておく。

k-bindk-bind

3.7.5 プリメティブなのにプロパティがある?

前提としてプリミティブはプロパティを持たない。
しかし、一見プリミティブがプロパティやメソッドを持っているように見える挙動が存在する。
(例えば文字列の持つlengthmatchなど)
これ実はプリミティブに対してプロパティアクセスを行うたびに一時的にオブジェクトが作られているため。プリミティブがプロパティを持っているようにみえるため次の内容も成り立ってしまう。

type HasLength = { length: number };
const obj: HasLength = "foobar";

これが成り立ってしまうのは文字列がlengthプロパティを持っている(?)ため。
つまりTypeScriptのオブジェクト型は実はその中身が本当にオブジェクトである保証をしない。

感想:
普段からlengthなど使う時に「プリミティブなのにプロパティ?」とモヤっとする時があったが、今回の説明でそのモヤモヤが晴れた。上の例からも可能な限りプリミティブの持つプロパティは避けた定義を心がけた方が安全なのかな。

k-bindk-bind

4.1.3 関数式で関数を作る

関数の引数に対しては分割代入を行う事が可能。

type Human = {
  height: number;
  weight: number;
}
const calcBMI = function({ height, weight }: Human): number {
  return weight / height ** 2;
};

「関数は値の一種」であるのでに変数に代入する事ができる。

感想:
分割代入を使う事で引数を渡す際もわざわざプロパティから取り出して渡す必要もなく、オブジェクト単体を渡せば良いためスッキリ書ける。またメソッドを読んだ時に何の型の何のプロパティを使っているのかが一目でわかるので使い勝手が良いと感じた。意識して使っていきたい。

k-bindk-bind

4.2.1 関数型の記法

関数型中の引数名は型チェックに影響しない。
ではなぜ存在しているかというと、エディタの支援機能を充実させるため。
関数の型はドキュメントの役割を果たすが、さらに引数名を書くことによって充実させる事ができる。
(型定義によりある関数の引数名が何を表しているのかをドキュメントとして表現する事ができる)
例えば、

type F = (repeatNum: number) => string;

という型定義がある場合、引数として与えられるrepeatNumは「何らかしら繰り返される数」であることが推測できる。

k-bindk-bind

4.2.3 返り値の型注釈は省略すべきか

その判断基準は"真実の源"(真実が型にあるのか、関数の中身にあるのか)で判断する。
返り値の型を明示する場合としない場合とでは”真実の源”が異なる。
返り値の型を

  • 明示する場合は、その型が絶対的な真実とみなされる。
  • 明示しない場合は、関数の中身が真実だとみなされる。

前者はコンパイラが返り値の型と関数の中身に矛盾がないかチェックするが、
後者は中身が真実であるので上記のチェックが当然かからない。
従って「この関数はこの型の値を返すべきである」という真実を用意するなら返り値の型を明示すべき。基本的には返り値の型を明示した方が有利。必ず明示する派閥が存在する。

k-bindk-bind

4.2.4 引数の型注釈が省略可能な場合

”逆方向の型推論”が働く場合は引数の型を省略する事ができる。
逆方向の型推論は、式の方が先にわかっている場合に、それをもとに指揮の内部に対して推論が働くこと。

type F = (arg: number) => string;

この場合に関数型Fの引数は数値型である事が示されているので

const xRepeat: F = (num) => "x".repeat(num);

と引数の型注釈を省略できる。
この逆方向の推論をTypeScriptではcontextual typingと呼ぶ。

k-bindk-bind

4.4.3 関数の型引数は省略できる

実際に引数に渡された値を型推論することで関数の型引数は省略可能である。
関数を使う側が型引数を意識する必要がないという意味で大きくジェネリック関数の利便性が向上する。言い換えると、関数を使う側にとっては型引数省略によって「好きな値で呼び出せば良い感じの型の返り値を返してくれる関数」として扱う事ができる。

k-bindk-bind

5.1.9 もう1つのプライベートプロパティ

TypeScriptのprivate修飾子はTypeScript独自の機能なのでコンパイル時のチェックしか使われないが、#はECMAScriptに由来するのでランタイムでもプライベート性が守られる。故に#を使った方ほうが厳格なプログラミングが書けるので迷ったら#にする。

k-bindk-bind

5.2.3 instanceof演算子と型の絞り込み

instanceofはオブジェクトがあるクラスのインスタンスかどうか判断する演算子。
そのインスタンスとはnew ClassName() で作られたオブジェクトを指す。
一方、const xxx: ClassName = ...で作成されたオブジェクトxxxはそのクラスのインスタンスとしてみなされない。(instanceofの判定もfalseとなる)単にxxxClassName型に適合するオブジェクトである。つまり同じ型=そのクラスのインスタンスとは限らないので注意したい。

k-bindk-bind

5.4.1 関数の中のthisは呼び出し方によって決まる

thisを使うオブジェクトのメソッドは原則としてメソッド記法で呼ぶべきである。
間違い:オブジェクト.メソッド名
↑メソッド中のthisundefinedとして評価されてしまう
正しい:オブジェクト.メソッド名()

またthisはクラスの専売特許ではないので以下のようにオブジェクトに定義されたメソッド内でthisを使うこともできる。

const user = {
  age: 26,
  isAdult() {
    return this.age >= 20;
  }
};
k-bindk-bind

5.4.2 アロー関数におけるthis

アロー関数はthisを外側の関数から受け継ぐ事ができる。
一方function関数式内のthisは外側から受け継ぐ事ができず、わざわざthisを別の変数に退避させてから使う必要がある。このthisが受け継げるかどうかの違いにより、アロー関数は優位な立ち位置を獲得し特に意図がない場合は通常アロー関数が使われるようになったそう

k-bindk-bind

6.1.5 オプショナルプロパティ再訪

プロパティが「あるかもしれないし、ないかもしれない」状況を表現する方法は以下の2通りの方法がある。

  1. オプショナルプロパティを使う
  2. 敢えてundefinedとのユニオン型を取る

前者の場合は利便性に優れる一方、「単に書き忘れた」のか区別できない場合がある。
そのような事を避けたい場合に2番目のユニオン型を取りundefinedを明示し、プロパティが不要な場合はundefinedを代入する。

つまり、プロパティが省略可かundefinedを敢えて代入するかの違いがあるが、実はオプショナルプロパティを使ってもundefinedは代入可ではある。どちらもundefinedを代入できると使い分けの境界線が曖昧となるので、その境界線を明確にし使い分けをハッキリさせる目的としてexactOptionalPropertyTypesというコンパイラオプションがある。これを有効にする事でオプショナルプロパティを使う場合(1の場合)undefinedが代入できなくなる。

k-bindk-bind

6.1.6 オプショナルチェイニングによるプロパティアクセス

undefinedが含まれるユニオン型を持つオブジェクトに対してメソッドやプロパティアクセスを実行したい場合、オプショナルチェインを使うと便利。対象のオブジェクトがundefinedだった場合はオプショナルチェイン以降の処理はスキップしてくれる。
例えば以下のような場合

obj?.foo["bar"]().baz().hoge;

objnullundefinedであった場合.foo以降の処理は全てスキップされる。
この?.foo["bar"]().baz().hogeまでのまとまりをオプショナルチェインと呼ぶ。

k-bindk-bind

6.3.2 typeof演算子を用いる絞り込み

typeofで型の絞り込みを行う際、typeof null"object"として返されるので注意。

k-bindk-bind

6.3.3 代数的データ型をユニオン型で再現するテクニック

代数的データ型は「判別用の情報(タグ)を持つ」という特徴があり、TypeScriptではこれを「リテラル型を持つプロパティ」として表現する。(リテラル型とはプリミティブ型をさらに細分化した型)
例えば、Animal型とHuman型、そしてそれを包含するUser型を擬似的な代数的データ型を使って表現したい場合は以下のように書く。

type Animal = {
  tag: "animal";
  species: string;
}
type Human = {
  tag: "human";
  name: string;
}
type User = Animal | Human;

ポイントはAnimal型とHuman型に共通のtagプロパティを持たせている事。この共通の判別用プロパティを持たせる事で擬似的な代数的データ型を実現する事ができる。そして、タグを頼りに型の絞り込みを行い、データの種類に応じた処理を行う事ができる。これはTypeScriptにおける極めて基本的なパターンであり、「扱うデータの形と可能性を型で正確に表現する」ということに大きく貢献している。

感想:

扱うデータの形と可能性を型で正確に表現する

という言葉が本全体を通じてとても印象に残った。
この言葉が当てはまる範疇は代数的データ型のセクションだけでなく、これまで学んできたことの目的や意図をギュッと集約した、TypeScriptで書く上で指針に近い一言なんじゃないかと感じた。

k-bindk-bind

6.4.2 keyof型とは

keyof型とは、オブジェクト型からそのオブジェクトのプロパティ名の型を得る機能。

type Human = {
  name: string;
  age: number;
};

がある時、keyof Human"name" | "age" というユニオン型を返す

keyofはtypeofと組み合わせることで機能を発揮する。
例えば

const user = {
  name: 'name',
  height: 180,
  weight: 77,
};

function requireProp(prop: keyof typeof user) {
  return user[prop]
};

ここでkeyof typeof user型は即ちtypeof user -> { name: 'name', height: 180, weight: 77 }keyofなので"name" | "height" | "weight"のユニオン型が最終的に返される。
つまりpropuserのプロパティ一覧のユニオン型を表す。従ってuser[prop]でユーザーのプロパティにアクセスできる。
以上からもわかる通り、typeof keyofを組み合わせるメリットとして以下が挙げられる。

  1. アクセス可能なプロパティに制限した型を作れる
  2. userオブジェクトにプロパティ追加などの変更があっても、関数の中身は書き換えずに済む。
k-bindk-bind

6.4.3 keyof型・lookup型とジェネリクス

keyofは型変数と組み合わせることでも威力を発揮する。

例えば

function get<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
};

この関数は「objのプロパティkeyの値を返す」関数である。

1つ目のポイントは<T, K extends keyof T>
これは「KTのプロパティ一覧を表すユニオン型(keyof T)の部分型(extends)である」
という事を意味する。
つまり、keyにはobjのプロパティ名(文字列)のいずれかが渡される事が期待される。

2つ目のポイントは返り値の型のT[K]obj[key]
key: Kにはobj: Tのプロパティ名が入るはずなのでobj[key]の期待される型はT[K]となる。

実際の使用イメージ:
humanというオブジェクトがweight: 60というプロパティを持っていたとすると
get(human, "weight")60が返される。

このように型引数に対してkeyofextendsを組み合わせて使う事で汎用性の高い(抽象度も高い)コードを書く事ができる。

k-bindk-bind

6.5.1 型アサーションを用いて式の型をごまかす

前提として型アサーションはできるだけ使用を避ける。
(TypeScriptが保証してくれる型安全を意図的に破壊する機能であるため)
ただし、時にはTypeScriptが型の絞り込みをうまく行ってくれない場合があるのでそういった場合には型の情報を補う目的で型アサーションを用いる。

例えば、

type Animal = {
  tag: "animal";
  species: string;
}
type Human = {
  tag: "human";
  name: string;
}
type User = Animal | Human;

function getNamesIfAllHuman(users: readonly User[]): string[] | undefined {
  if (users.every(user => user.tag === "human")) {
    return (users as Human[]).map(user => user.name);
  }
}

例えばこちらの例では、users.every(user => user.tag === "human")trueである場合は、すなわち「usersの全要素がHuman型である = Human[]型である」事を意味しているのだがコンパイラはその理屈まで追って理解する事ができない。従ってtrueならusersHuman[]型であるということを明示的に補足する必要がある。
そうすることでnameプロパティにアクセス可能となる(明示しない場合はAnimal型である可能性も考慮されコンパイルエラーが発生する)。

このようにasによるアサーションはコンパイラが追いきれない型情報を補足する目的で用いる。

k-bindk-bind

コメントありがとうございます!
条件を満たす場合に型の絞り込みが行われるユーザー定義ガードを活用しているのですね。
仰る通りasを使わず、かつ条件内で型の絞り込みも完結するこちらの方が選択肢として良さそうだと思いました。大変勉強になりました!

k-bindk-bind

6.7.2 型述語(ユーザー定義ガード)

ユーザー定義ガードは、返り値の型として型述語が書かれた特殊な関数。

function isStringOrNumber(value: unknown): value is string | number {
  return typeof value === "string" || typeof value === "number";
}

返り値の型の箇所のvalue is string | numberが型述語と呼ばれ、このように書くとunknown型の値valuestring型またはnumber型である場合にtrueを返す関数となる。ユーザー定義ガードを無事通過すると、valueの型はunknownからstring | numberとなる。
また、返り値の型はbooleanではなく型述語で書く必要があることに注意。

ユーザー定義型ガードはanyasの仲間であり、型安全性を破壊する恐れのある危険な機能である。使用した時点で「型安全性に対する責任がTypeScriptコンパイラから我々に移っている」事をしっかり意識して使う事。
ただユーザー定義ガードはany型やasに比べ「我々の責任において何を保証すれば良いのかが明確」であるため、危険な機能が必要な場合は一番積極的に選ぶ選択肢でもある。

k-bindk-bind

8.3.4 自分でPromiseオブジェクトを作る

Promiseの内部処理はコールバックベースであり、Promiseはコールバックベースの非同期処理をうまく隠蔽してくれる。
例えば

const p = new Promise<number>((resolve) => {
  setTimeout(() => {
    resolve(100);
  }, 3000);
}

このPromiseはsetTimeoutというコールバックベースの非同期処理を隠蔽している。
executor関数には引数として関数を渡しresolveという名前が通例となっている。setTimeout関数により3秒後にresolveが呼ばれ、成功裡に解決、resolveの中身である100.then()内に渡される。つまりresolveとは「Promiseが用意したコールバック関数である」ともみなせる。
上の例では100を渡すのでPromise<number>となる、何も渡さない場合はPromise<void>と書く。

k-bindk-bind

8.3.9 Promiseチェーン(2)非同期処理の連鎖

Promiseがネストすることはなく、「PromiseのPromise」は作られない。
例えば、

p3 = p1.then((result) => {
  const p2 = new Promise<...>(...) // <- 何かPromiseが返される処理
  return p2 
});
p3.then((result) => {
  ... // <- p3はp2の結果そのものなので、p2が解決したタイミングで実行される
})

この場合、「p3はPromisep2を結果とするPromise(.then())」(つまりPromiseのネスト)ではなく「p3p2の結果そのもの」となる。p3p2の結果そのものなので、p2が解決されると同時にp3も解決される。このようにp1 -> p2 -> p3 と順番に非同期処理を実行したい場合にPromiseチェーンは役立つ。

k-bindk-bind

8.3.10 Promiseチェーン(3)エラーの扱い

Promiseチェーンを作る場合、必然的に末端のPromiseは何もコールバック関数が登録されていないPromiseとなる。従って、この末端のPromiseは必ず成功するようにする事が重要。
失敗する可能性のあるPromiseは必ずcatchなどによるエラー処理(失敗-> 成功の変換)を行うよう心がける。