プロを目指す人のためのTypeScript入門 学習メモ #ブルーベリー本
学習の記録用です。知らなかった事、記憶に留めたいポイントを中心に記録します。
(感想は必要に応じて残します)
3.1.6 オブジェクトはいつ"同じ"なのか
スプレッド構文のコピーは変数間でオブジェクトが共有されるので注意。
またネストしたオブジェクトも含め全てコピーする標準的な方法は存在しない。
3.2.4 interface宣言でオブジェクト型を宣言する
interface宣言はtype文で代用可能であり、ほとんどの局面ではtype文が使われる。
そのためtype文のみ使うという流儀も存在する。
type文では任意の方に対して別名で宣言可能。
一方、interface宣言で扱えるのはオブジェクト型だけ。
3.3.3 余剰プロパティに対する型エラーについて
余剰プロパティに関するエラーは、型安全性に関するエラーとは関係がない。
型安全性な状態でも余剰プロパティエラーは発生しうる。
(部分型関係はプロパティ包含関係の条件が満たされていればOKなので)
余剰プロパティエラーは「このプロパティはアクセスできないから存在しても意味ないよ」という旨のエラー。
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.foo
がundefined
であるため。
対象プロパティの値がundefined
ならデフォルト値、既に値が入っているなら既定値が入ると覚えておく。
3.7.5 プリメティブなのにプロパティがある?
前提としてプリミティブはプロパティを持たない。
しかし、一見プリミティブがプロパティやメソッドを持っているように見える挙動が存在する。
(例えば文字列の持つlength
やmatch
など)
これ実はプリミティブに対してプロパティアクセスを行うたびに一時的にオブジェクトが作られているため。プリミティブがプロパティを持っているようにみえるため次の内容も成り立ってしまう。
type HasLength = { length: number };
const obj: HasLength = "foobar";
これが成り立ってしまうのは文字列がlength
プロパティを持っている(?)ため。
つまりTypeScriptのオブジェクト型は実はその中身が本当にオブジェクトである保証をしない。
感想:
普段からlength
など使う時に「プリミティブなのにプロパティ?」とモヤっとする時があったが、今回の説明でそのモヤモヤが晴れた。上の例からも可能な限りプリミティブの持つプロパティは避けた定義を心がけた方が安全なのかな。
4.1.3 関数式で関数を作る
関数の引数に対しては分割代入を行う事が可能。
type Human = {
height: number;
weight: number;
}
const calcBMI = function({ height, weight }: Human): number {
return weight / height ** 2;
};
「関数は値の一種」であるのでに変数に代入する事ができる。
感想:
分割代入を使う事で引数を渡す際もわざわざプロパティから取り出して渡す必要もなく、オブジェクト単体を渡せば良いためスッキリ書ける。またメソッドを読んだ時に何の型の何のプロパティを使っているのかが一目でわかるので使い勝手が良いと感じた。意識して使っていきたい。
4.2.1 関数型の記法
関数型中の引数名は型チェックに影響しない。
ではなぜ存在しているかというと、エディタの支援機能を充実させるため。
関数の型はドキュメントの役割を果たすが、さらに引数名を書くことによって充実させる事ができる。
(型定義によりある関数の引数名が何を表しているのかをドキュメントとして表現する事ができる)
例えば、
type F = (repeatNum: number) => string;
という型定義がある場合、引数として与えられるrepeatNum
は「何らかしら繰り返される数」であることが推測できる。
4.2.3 返り値の型注釈は省略すべきか
その判断基準は"真実の源"(真実が型にあるのか、関数の中身にあるのか)で判断する。
返り値の型を明示する場合としない場合とでは”真実の源”が異なる。
返り値の型を
- 明示する場合は、その型が絶対的な真実とみなされる。
- 明示しない場合は、関数の中身が真実だとみなされる。
前者はコンパイラが返り値の型と関数の中身に矛盾がないかチェックするが、
後者は中身が真実であるので上記のチェックが当然かからない。
従って「この関数はこの型の値を返すべきである」という真実を用意するなら返り値の型を明示すべき。基本的には返り値の型を明示した方が有利。必ず明示する派閥が存在する。
4.2.4 引数の型注釈が省略可能な場合
”逆方向の型推論”が働く場合は引数の型を省略する事ができる。
逆方向の型推論は、式の方が先にわかっている場合に、それをもとに指揮の内部に対して推論が働くこと。
type F = (arg: number) => string;
この場合に関数型F
の引数は数値型である事が示されているので
const xRepeat: F = (num) => "x".repeat(num);
と引数の型注釈を省略できる。
この逆方向の推論をTypeScriptではcontextual typingと呼ぶ。
4.4.3 関数の型引数は省略できる
実際に引数に渡された値を型推論することで関数の型引数は省略可能である。
関数を使う側が型引数を意識する必要がないという意味で大きくジェネリック関数の利便性が向上する。言い換えると、関数を使う側にとっては型引数省略によって「好きな値で呼び出せば良い感じの型の返り値を返してくれる関数」として扱う事ができる。
5.1.9 もう1つのプライベートプロパティ
TypeScriptのprivate修飾子はTypeScript独自の機能なのでコンパイル時のチェックしか使われないが、#
はECMAScriptに由来するのでランタイムでもプライベート性が守られる。故に#
を使った方ほうが厳格なプログラミングが書けるので迷ったら#
にする。
5.2.3 instanceof演算子と型の絞り込み
instanceof
はオブジェクトがあるクラスのインスタンスかどうか判断する演算子。
そのインスタンスとはnew ClassName()
で作られたオブジェクトを指す。
一方、const xxx: ClassName = ...
で作成されたオブジェクトxxx
はそのクラスのインスタンスとしてみなされない。(instanceof
の判定もfalse
となる)単にxxx
はClassName
型に適合するオブジェクトである。つまり同じ型=そのクラスのインスタンスとは限らないので注意したい。
5.4.1 関数の中のthisは呼び出し方によって決まる
thisを使うオブジェクトのメソッドは原則としてメソッド記法で呼ぶべきである。
間違い:オブジェクト.メソッド名
↑メソッド中のthis
がundefined
として評価されてしまう
正しい:オブジェクト.メソッド名()
またthis
はクラスの専売特許ではないので以下のようにオブジェクトに定義されたメソッド内でthis
を使うこともできる。
const user = {
age: 26,
isAdult() {
return this.age >= 20;
}
};
5.4.2 アロー関数におけるthis
アロー関数はthis
を外側の関数から受け継ぐ事ができる。
一方function関数式内のthis
は外側から受け継ぐ事ができず、わざわざthis
を別の変数に退避させてから使う必要がある。このthis
が受け継げるかどうかの違いにより、アロー関数は優位な立ち位置を獲得し特に意図がない場合は通常アロー関数が使われるようになったそう
6.1.5 オプショナルプロパティ再訪
プロパティが「あるかもしれないし、ないかもしれない」状況を表現する方法は以下の2通りの方法がある。
- オプショナルプロパティを使う
- 敢えて
undefined
とのユニオン型を取る
前者の場合は利便性に優れる一方、「単に書き忘れた」のか区別できない場合がある。
そのような事を避けたい場合に2番目のユニオン型を取りundefined
を明示し、プロパティが不要な場合はundefined
を代入する。
つまり、プロパティが省略可かundefined
を敢えて代入するかの違いがあるが、実はオプショナルプロパティを使ってもundefined
は代入可ではある。どちらもundefined
を代入できると使い分けの境界線が曖昧となるので、その境界線を明確にし使い分けをハッキリさせる目的としてexactOptionalPropertyTypes
というコンパイラオプションがある。これを有効にする事でオプショナルプロパティを使う場合(1の場合)undefined
が代入できなくなる。
6.1.6 オプショナルチェイニングによるプロパティアクセス
undefined
が含まれるユニオン型を持つオブジェクトに対してメソッドやプロパティアクセスを実行したい場合、オプショナルチェインを使うと便利。対象のオブジェクトがundefined
だった場合はオプショナルチェイン以降の処理はスキップしてくれる。
例えば以下のような場合
obj?.foo["bar"]().baz().hoge;
obj
がnull
やundefined
であった場合.foo
以降の処理は全てスキップされる。
この?.foo["bar"]().baz().hoge
までのまとまりをオプショナルチェインと呼ぶ。
6.3.2 typeof演算子を用いる絞り込み
typeof
で型の絞り込みを行う際、typeof null
は"object"
として返されるので注意。
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で書く上で指針に近い一言なんじゃないかと感じた。
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"
のユニオン型が最終的に返される。
つまりprop
はuser
のプロパティ一覧のユニオン型を表す。従ってuser[prop]
でユーザーのプロパティにアクセスできる。
以上からもわかる通り、typeof keyof
を組み合わせるメリットとして以下が挙げられる。
- アクセス可能なプロパティに制限した型を作れる
-
user
オブジェクトにプロパティ追加などの変更があっても、関数の中身は書き換えずに済む。
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>
これは「K
はT
のプロパティ一覧を表すユニオン型(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
が返される。
このように型引数に対してkeyof
とextends
を組み合わせて使う事で汎用性の高い(抽象度も高い)コードを書く事ができる。
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
ならusers
はHuman[]
型であるということを明示的に補足する必要がある。
そうすることでname
プロパティにアクセス可能となる(明示しない場合はAnimal
型である可能性も考慮されコンパイルエラーが発生する)。
このようにas
によるアサーションはコンパイラが追いきれない型情報を補足する目的で用いる。
コメントありがとうございます!
条件を満たす場合に型の絞り込みが行われるユーザー定義ガードを活用しているのですね。
仰る通りas
を使わず、かつ条件内で型の絞り込みも完結するこちらの方が選択肢として良さそうだと思いました。大変勉強になりました!
6.7.2 型述語(ユーザー定義ガード)
ユーザー定義ガードは、返り値の型として型述語が書かれた特殊な関数。
function isStringOrNumber(value: unknown): value is string | number {
return typeof value === "string" || typeof value === "number";
}
返り値の型の箇所のvalue is string | number
が型述語と呼ばれ、このように書くとunknown
型の値value
がstring
型またはnumber
型である場合にtrue
を返す関数となる。ユーザー定義ガードを無事通過すると、value
の型はunknown
からstring | number
となる。
また、返り値の型はboolean
ではなく型述語で書く必要があることに注意。
ユーザー定義型ガードはany
やas
の仲間であり、型安全性を破壊する恐れのある危険な機能である。使用した時点で「型安全性に対する責任がTypeScriptコンパイラから我々に移っている」事をしっかり意識して使う事。
ただユーザー定義ガードはany
型やas
に比べ「我々の責任において何を保証すれば良いのかが明確」であるため、危険な機能が必要な場合は一番積極的に選ぶ選択肢でもある。
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>
と書く。
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のネスト)ではなく「p3
はp2
の結果そのもの」となる。p3
はp2
の結果そのものなので、p2
が解決されると同時にp3
も解決される。このようにp1 -> p2 -> p3 と順番に非同期処理を実行したい場合にPromiseチェーンは役立つ。
8.3.10 Promiseチェーン(3)エラーの扱い
Promiseチェーンを作る場合、必然的に末端のPromiseは何もコールバック関数が登録されていないPromiseとなる。従って、この末端のPromiseは必ず成功するようにする事が重要。
失敗する可能性のあるPromiseは必ずcatchなどによるエラー処理(失敗-> 成功の変換)を行うよう心がける。