TypeScriptのtype-challenges(easy)をやってみた
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を使う
K
がT
のプロパティ名の型である時、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 TypeT[P]
においてはP
はkeyof 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カテゴリにもチャレンジしたい。