Akihito Ikeda

TypeScriptのtype-challenges(easy)をやってみた

posts/2021-04-05diary

TypeScriptに少し興味があったものの特になにも勉強せずぼんやりと日々を過ごしていたところ、いくつか型に関する記事を見かけてさらに type-challenges というものを知ったので、とりあえずeasyカテゴリの問題をやってた。

意気揚々とやってみたはいいもののさっぱりわからずぴえん🥺となったので答えをみつつ学んだことをメモしておく。

Pick

type MyPick<T, K extends keyof T> = {
  [P in K]: T[P]
}
  • 既存のオブジェクト型(のプロパティ)に対して一括で何かやりたいという時にはMapped Typesが使える

    • { [P in K]: T }という構文
    • このままだと、全てのプロパティの型がTになる
  • Lookup Typesを使う

    • KTのプロパティ名の型である時、T[P]はそのプロパティの型となる
    • これをMapped TypeのTにあてはめると{ [P in K]: T[P] }となる

ちなみにMapped Typesの基本形である{ [P in K]: T }では、それぞれ

  • P: 引数型
  • K: 制約型
  • T: テンプレート型

と呼ぶらしい(ref. Mapped Typesのあれこれ

Readonly

type MyReadonly<T> = {
  readonly [P in keyof T]: T[P]
}
  • Mapped Typeではreadonly, ?のような修飾子が使える
  • さっきのMyPickに似ているが、MyPick<T, K extends keyof T> では制約型(Mapped Type { [P in K]: T } におけるK)のベース制約が keyof T になっているのに対し、こちらは制約型そのものが keyof T となっている
  • MyPickとMyReadonlyの定義(型変数)によってkeyof ...をつける場所がかわっているだけで、いずれにせよLookup Type T[P] においては Pkeyof T の部分型である必要がある

Tuple to Object

type TupleToObject<T extends readonly any[]> = {
  [P in T[number]]: P
}
  • T[number]というのは配列Tの要素の型を表す
  • 配列であるTに対してnumber型のプロパティ名でアクセスできるプロパティの型ということ

First of Array

type First<T extends any[]> = T[number] extends never ? never : T[0]
  • Conditional Typesというやつ
  • T extends U ? X : Yという構文
  • T[0] みたいに書けるのは驚いた
type First<T extends any[]> = T extends [infer X, ...infer rest] ? X : never
  • こんなこともできるんだ…すごい
  • 真のときにrestの方を返せばTail型みたいなのも作れる
  • inferというのは、Conditional Typeの条件部分で型変数を新しく導入するキーワード

Length of Tuple

type Length<T extends readonly any[]> = T['length']
type Length<T extends {length: number}> = T['length'];
  • タプル(配列)のlengthプロパティを参照してる

Exclude

type MyExclude<T, U> = T extends U ? never : T
  • 一見するとTがunion型だったとき T extends U ? の条件判定でなぜExclude(“除外”)的な動作をするのかわからない
  • これは union distribution(union型の分配)というものが働いてるようだ
  • union distributionとは簡単に言うと、「Conditional Typeのextendsの左側が型変数で、そこにunion型が来るとき、分配して条件判定が行われる」という動作のこと

    • ※ 正確ではないかもしれないので注意
  • 例えば T extends U ? never : X の場合

    • T"a" | "b" というunion型が来ると、("a" | "b") extends U ? never : X ではなく、("a" extends U ? never : X) | ("b" extends U ? never : X) というように分配される
    • 分配されたものがそれぞれ評価されて最終的にunion型になる
  • union distributionが起こるのはTが型変数のときだけで、例えば SomeUnion extends U みたいに型を直接書くと時は起こらない

※ union distributionについては TypeScriptの型初級 - Qiita がわかりやすかった

Awaited

type Awaited<T> = T extends Promise<infer P> ? P : T
  • Promise<T>の型変数をConditional Typeのthenで使いたいのでinferしている

If

type If<C extends boolean, T, F> = C extends true ? T : F
  • 書かれてみれば「なるほど」という感じだ

Concat

type Concat<T extends any[], U extends any[]> = [...T, ...U]
  • 型でこんなことまで表現できるのすごい

Includes

type Includes<T extends readonly any[], U> = U extends T[number] ? true : false
  • T[number]を使えばこんなことも表現できる

以上がeasyカテゴリの問題。むずすぎ…と思ったけど、別に型を組み合わせて複雑な定義がなされてるわけじゃなくて、型の機能をそのまま使ってるだけなので慣れればさくさく脳内で処理できるものなのかな。 まだeasyとはいえこの時点でもにかく表現力がすごいと感じたけど、実際のコードでどこまで使われてるんだろうか。

まあどれくらい使われてるとか何かに活きるとかおいといて純粋にパズルとして楽しかったのでmediumカテゴリにもチャレンジしたい。

© Akihito Ikeda - Last update 06.05.2021 23:27.