「TypeScriptの型初級」で手を動かす

「TypeScriptの型入門」の続編である「TypeScriptの型初級」の記事を読みながら、プレイグラウンドで手を動かして行きたいと思います。

qiita.com

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があれば、それは除外
  • 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】オーバーロードの様々な書き方 - Qiita

プレイグランド(関数オーバーロード)

this

  • 関数定義や関数の型を書くときに「this」の型を明示することができる
  • thisの型は最初の引数に書いて明示する
    • ただし本当の引数でないため、thisを渡すわけではない
  • ただしあまり使われないらしい
    • contexualな型推論のために使われることが多いとのこと
      • jQueryみたいに、コールバック関数内でthisの値を書き換える場合にthisの型を正しく推論させるために型指定するみたいな
    • jQueryだとelementが引数にcallbackが呼ばれることがあるが、thisがelementだと推論できるとかか。

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とかやっていこうっと。