「TypeScriptの型初級」で手を動かす
「TypeScriptの型入門」の続編である「TypeScriptの型初級」の記事を読みながら、プレイグラウンドで手を動かして行きたいと思います。
TypeScriptの型初級の記事の目標
- 実用上必要となるTypeScriptの型の挙動を理解する
- 標準ライブラリに存在する型の使い方を理解する
前回の記事が入門編ということだが、内容的には結構濃かったと思う。 まぁ、全部とまでは言わないが、ほぼほぼ理解はできたと思っているので(忘れていると思うけど)、次のステップにすすみつつ、復習していき、自分のスキルにしていきたいっすね。
union型の復習
- 初級編のひとまずの主役はunion型らしい
- union型は
T1 | T2 | T3
のように複数の型を|
で繋げた型- T1, T2, T3のいずれかである値の型という意味
- union型のいいところ
- if文やswitch文などで実行時に型を判定するコードを書くと型が絞られる
conditional typeにおけるunion distribution
- ことばで説明するのがむずい概念。
- 「union型の条件型」が「条件型のunion型」に変換される
T3 = T1 | T2
で、T3 extends A ? B : C
, みたいな条件の場合に( T1 extends A ? B: C) | (T2 extends A ? B:C)
とunion型が分配される
条件型の結果側における型変数の置き換え
- 条件部だけでなく、結果にも型変数が使える
type NoneToNull<V extends Option<unknown>> = V extends Some<unkown> ? V : null |;
こういう書き方もできるのねー、という感想しかない。。 多分あまり腑に落ちてないだろうな。
分配されるのは型変数のみ
- union distributionが発生するのは条件部分の型が型変数である場合のみ
V extends Some<unknown> ...
みたいな。
- 型引数を持つような型を型関数という
- 型関数をインライン化するだけで結果が変わる。。(非直感的らしい...)
- union distributionさせることを意図しているのかそうじゃないのか人のコードを見るときに考える必要がある
- 型変数で条件分岐したいけど、union型が来ても分配してほしくない場合
何か適当な型で囲む
- なんじゃこりゃ
- 型変数だけになるとunion distributionされる。
- TじゃなくてT[] とか配列にするとunion disributionされない
- 配列型で囲むのが記法が簡単なのでよく使われる
- こんなのがよく使われるのかぁ
V[] extends Some<infer R>[] ? R : undefined
にするとunion distributionされないらしい
never型とunion distiribution
- never型は属する値が無い型。union disributionでは特殊な振る舞いをする
- never型は0個のunion型であるように振る舞う
- ぐぬぬ。どういうことでしょうか。。
T extends never ? X : Y
のとき、Tにneverを代入すると結果はneverになる。- なぞ、これはよくわからん
union distributionのまとめ
- 条件型の条件部分の型が型変数ならばunion型が分配される
- 直感に反するが、これがあるからunion型を便利に使えるらしい
- 後半で説明があると、楽しみ
- exendsの左が型変数になるのを特別扱いする理由は、unionを分配したときに結果側もかきかえなければいけないから
- うーん???
- ちょっとむずいね
プレイグラウンドで手を動かす(union distributionまで)
mapped typeのunion distribution
- mapped typeもunion型を分配する
- 分配が発生する条件はconditional typeのときよりも複雑
{ [P in keyof T] : X }
のTが型変数のとき、Tにunion型が入ると分配される[ P in keyof T]
は頻出する形なので、そのときにunion型だったらどうなるかとか意識する必要がある
mapped typeと配列型
[ P in keyof T ] : A
という形で、T
に配列の型(タプル型も含む)が入る場合も特別な挙動をする- T = number[]とかの場合、forEachなど関数型もあるが、普通にやると、forEachもA型になるため、関数として実行できなくなる。
- 型変数の場合は、forEachは関数型として残してくれる。
- 型変数Tが配列型の場合、プロパティをマップするのではなくて、要素の型のみをマップしてくれる
type Strify<T> = { [ P in keyof T ] : string } // 本来の挙動は、T型のすべてのプロパティをstring型にする type NumArr = number[] type StrArr = Strify<NumArr>; // StrArr は string[] 型になる。要素の型(string)だけがマップされた // number型のそれぞれの要素がstring型になるので、string[]になるということかな
- Strify
は普通のmapped type定義で、オブジェクト型にそのまま使える - 配列を特別扱いしなくてもよいのがいい感じってことかな
ちなみに、この機能がTが型変数の場合に制限されているのは、そうしないとマップ後の配列の要素の型を正しく求められない場合があるからでしょう。{[P in keyof T]: X}という型(Xは型変数とは限らない任意の型)で配列型U[]をマップした場合、要素の型はX内のT[P]をUで置換したものになります。この形のmapped typeならば、要素の型をマップする際にXの内部にもともとの要素の型はシンタクティックにT[P]という形で現れるためそれをUに置換すればいいわけです(ほかにT[number]とかもあるかもしれませんが、それは置換せずそのままでOKです)。Tが一般の型になってしまうとこのような変換が難しくなります。
この部分、わかんねー、
- タプル型も配列型の一種なので同じルールが適用
- オブジェクト型じゃなくて、タプル型に変換される???
うーん、なんとなく理解できているが、説明は難しいというレベルの理解度だな。
readonly配列型との変換
- 標準の「Readonly
」を利用して、オブジェクトのプロパティをreadonlyにできるけど、配列やタプル型を指定してもreadonlynになる - うーん、この理屈がまだ理解できてないな。
標準ライブラリの型
- いつでも使える便利な型ライブラリ
- ユーティリティ型が定義されているlib.es5.d.ts
最新のlib.es5.d.ts
- Omitとか追加されている
Record
- 辞書型として使える
const dict: Record<string, number>
- 存在しないキーがundefinedを返す可能性を無視していることに注意
Record<string, number | undefined>
とするかMapを使う
K extends keyof any
のkeyof anyはstring | number | symbol
と同値Partial
- プロパティをすべて任意にする
[P in keyof T]? : T[P]
Required
- プロパティをすべて必須にする
[P in keyof T]-?
: T[P]`-?
で任意指定を削除できる
Readonly
- プロパティを参照専用にする
readonly [P in keyof T] : T[P]
Pick<T, K>
- 一部のプロパティを取得する
- 既存の型をちょっといじった新しい型を作りたい場合によく使われる
Exclude<T, U>
- union distributionを前提としている
- Tに何らかのunion型が入ったとき、その構成要素のうちUの部分型を除外する
T extends U ? never : T
T = 'foo' | 0
の場合- ('foo' extends string ? never : 'foo') | ((0 extends string ? never : 0)
- condtional typeに合致したらneverなので、部分型なら除外される
Extract<T, U>
- union distributionを前提としている
- Tに何らかのunion型が入ったとき、その構成要素のうちUの部分型を残す
これらは、union型で代数的データ型ぽいことをやる場合に役立つ
Omit<T, K extends keyof T>
- 指定したプロパティを除外したオブジェクト型を作る
- TypeScript3.5で追加になったっぽい
- PickとExcludeを駆使して実現されている
Pick<T, Exclude<keyof T, K>>
NonNullable
- Tからnullまたはundefinedを除外
- 別にnullやundefinedのプロパティを除外するというわけではない
- union型の要素としてnull/undefinedがあれば、それは除外
- Tからnullまたはundefinedを除外
Parameters
- 関数に関わる型
- Tは関数型でなければならない
- Tの引数の型一覧をタプル型の形で取得できる
T extends (...args: infer P) => any ? P : never
- イマイチだな理解がこれは
- Tが関数の型だったらPを返す。Pはタプル型になるんぁ?
ReturnType
- 関数に関わる型
- 返り値の型を取得する
T extends (...args: any[]) => infer R ? R : any
- Tが関数の型ならその戻り値の型Rを返す。
関数のオーバーロード
- TypeScriptでは関数のオーバーロードを定義することができる
- JavaScriptにはその機能はないので、型だけオーバーロードできる
- 関数本体が無い関数宣言だけ書く(オーバーロード)
- 関数本体の宣言では型はオーバーロードされた関数の全てに当てはまる包含的な型の必要がある
arg2!
の!
はTypeScript独自の演算子。nullやundefinedを取り除くダウンキャスト演算子- 使いにくいから筆者はあまりオーバーロードされた関数定義は好きでないと
- ただし、既存のJSライブラリに型を付ける際によく登場する
- オブジェクト型を用いて関数の型を宣言するときも、同様に関数シグネチャを複数ならべてオーバーロードされた関数の型を表現できる
- interfaceの関数型でもオーバーロードできる
参考 【TypeScript】オーバーロードの様々な書き方 - Qiita
this
- 関数定義や関数の型を書くときに「this」の型を明示することができる
this
の型は最初の引数に書いて明示する- ただし本当の引数でないため、
this
を渡すわけではない
- ただし本当の引数でないため、
- ただしあまり使われないらしい
this型
- クラス(やインターフェース)のメソッド内で使うことができる特殊な型
- cloneメソッドを実装するときに使われる
- 継承元でcloneを実装して返り値の型が親クラスになると都合が悪い
public clone(): this
というように、thisを型として指定できる
- うーむ、まぁ、あるのか。
カスタム型ガード
- if文とtypeofを組み合わせて型の絞り込みができる
- 型の絞り込みを行う用の関数を自分で定義できる
isFooObj(arg: any): arg is FooObj
みたいに定義可能- 返り値はboolean
- そんなにたくさん使われるものでもない
- 標準ライブラリの
Array.isArray
に使われているisArray(arg: any): arg is Array<any>;
まとめ
- union型は強力
- union型を積極的に使っていこう
次のステップ
色々あるので、時間を見つけて見ていこうかな。
途中だれて、時間かかっちゃったけど、一通り読めた!
理論系は一旦一区切りして、なにか実装したりTypeChallengeとかやっていこうっと。