type-challenges メモ
type-challengesをレベル別に順にやっていって調べたこととかメモ。自分が解けたものについては載せません。
これをやる時点でのレベルは簡単な型定義を書けるくらいなので、不正確なこととかあればマサカリ歓迎です。
00018-easy-tuple-length
lengthをどうやって取ればいいのかと思ったが、 length
というキーを参照すればいいだけっぽい。
その時に引数がarrayになっていることは保証しておけばOK。
type Length<T extends readonly any[]> = T['length']
00043-easy-exclude
conditional types (3項演算子のように型を分岐)では、Union型を受け取ると展開して判定される。
TもUもUnion型の場合、多項式の展開のように全パターンで分岐を判定し、結果を再度Unionしたものが返される。
type MyExclude<T, U> = T extends U ? never : T
00189-easy-awaited
Javascriptでは then
メソッドを持っていると Promise
扱いされる。
Typescriptではこういった Promise
的なものに対して PromiseLike
という型が提供されており、それを使用するとよい。
今回の問題では、Promise<Promise<...>>
のように Promise
が多重に包含されている場合は全ての Promise
を剥がさないといけない。
そのため、再帰的に定義して一番内側の方を取得する必要がある。
さらに、PromiseLike
でない型を入力するとエラーにする必要があるので、解法としては以下のようになる。
- 再帰的に
PromiseLike
を剥がす型 -
PromiseLike
以外を受け取らないよう制限する型
type MyAwaitedBase<T> = T extends PromiseLike<infer U> ? MyAwaitedBase<U> : T
type MyAwaited<T extends PromiseLike<any>> = MyAwaitedBase<T>
00898-easy-includes
パッと思いついた解法は「一旦 T
の中身をキーとするオブジェクトを作り、そのオブジェクトが U
を含んでいるかを判定する」。
アルゴリズム的なことを記述しないのでこういう書き方をしたいが、 boolean
や undefined
などを受け取るとうまくいかない。
したがって、結局はarrayの中身を舐めていって1つずつ等価判定をしていく必要がある。
等価判定を行う型はtypescript-challengeで提供されている Equal
を一旦使うとして、以下のような流れで再帰的に判定していく。
- 受け取ったarrayを最初の要素とそれ以外に分ける
- 最初の要素で等価判定して等しければ
true
を返す - 等しくなければ、2個目以降の要素のみを対象として1.を実行する
- そもそも最初の要素を取り出せない場合は
false
type Includes<T extends readonly any[], U>
= T extends [infer F, ...infer R]
? (Equal<F, U> extends true ? true : Includes<R, U>)
: false
等価判定についてはtypescript-challengeの Equal
を自前で実装すればよい。
ロジックの理解についてはこちらの記事で解説されているのがわかりやすかった。
00003-medium-omit
ここから中級。Omitを自前実装する問題。
素朴に考えると「キーを舐めていって残すか除外するか判定する」というようなことを思い浮かべてしまうが、Key Remappingというものを使用すると比較的簡単に実現できる。
Key RemappingはMapped Typesのドキュメントに記載がある。
端的にいうとMapped Typesでのキーをリネームできてしまうもので、リネームする代わりに never
を使用することで「残す・残さない」のような挙動が実現できる。
Mapped Typesで key in keyof T
のようなものの後に as ~~~
と記載する。
この as
は型アサーションの as
とは全くの別物なので注意が必要。
type MyOmit<T, K extends keyof T> = { [k in keyof T as k extends K ? never : k]: T[k] }
00008-medium-readonly-2
指定したキーのみreadonlyにする問題。キーを指定しない場合は全部のキーについてreadonlyにする。
まず、「キーを指定しない場合は全部のキーについてreadonlyにする」とあるので、デフォルト型引数を指定する必要がある。
今回はデフォルト値として keyof T
のように全てのキーを指定しておけばいい。
「指定したキーのみreadonlyにする」というのは、言い換えれば「指定していないキーはそのまま」であるので、 (指定していないキーについての型) & (指定したキーについての型)
のように交差型で実装すればよい。
この場合の「指定していないキーについての型」は Omit
を使って取り出せばいい(ずるい気がする場合は初級問題通りに実装すればいい)。
「指定したキーについての型」に関しては初級問題同様にreadonlyを指定していけばよい。
type MyReadonly2<T, K extends keyof T = keyof T> = Omit<T, K> & { readonly [k in K]: T[k] }
00009-medium-deep-readonly
再帰の問題。愚直解は単純だが、スマートに解こうとするとちょっとハマりやすい。
愚直解
Mapped typesのvalueを DeepReadonly
とすることで再帰的にreadonlyを付与し、プリミティブ型を受け取った場合にはそのまま返すような終了条件とすればよい。
プリミティブ型として何を設定するかだが、テストケースにあるものを用意しておけば、この問題を解くだけならOK(もちろん実務ではダメ)。
type DeepReadonly<T> = T extends string | number | Function ? T : { readonly [k in keyof T]: DeepReadonly<T[k]> }
スマートな解
プリミティブ型の判定部分がさすがに良くないので、スマートな方法を考える。
オブジェクトかどうかを判定すればよいので、キーを取得してみてダメならプリミティブ型、というようなことをするとよさそう。
ただ、愚直解の判定部分をそのまま置き換えただけではcase2のようにユニオン型を受け取った場合に通らない。
// ダメな例。ユニオン型でうまく動かない。
type DeepReadonly<T> = keyof T extends never ? T : { readonly [k in keyof T]: DeepReadonly<T[k]> }
keyof
の対象がオブジェクトのユニオンである場合、すべてのオブジェクトに共通して存在するキーのみが取得される。
そのため、型引数としてオブジェクトのユニオンを受け取った場合にうまく動かない。
type Hoge = keyof ({ a: string, b: string } | { a: number, c: string })
// type Hoge = "a"
ユニオン型のそれぞれに対して別々に判定処理をして欲しいので、Mapped types部分で判定を行えば、よしなに動作してくれる。
以下のようにすればうまく動く。
type DeepReadonly<T> ={ readonly [k in keyof T]: keyof T[k] extends never ? T[k] : DeepReadonly<T[k]> }
00012-medium-chainable-options
「option
で型を追加し get
で型を得る」というメインの挙動と、「すでに追加されたキーを追加しようとするとエラーになるが、実行すると上書きされる」という制約の挙動という、2段階に分けて実装を考える。
メインの挙動の実装
option
の実行結果もまた元の Chainable
になるので、少し再帰のような挙動になる(本当は再帰でもなんでもないが)。
この際、option
を実行することで追加された型定義の情報を保持しておく必要がある。
この情報は型引数で受け取ることで保持し、初期値を空オブジェクトにしておけば実現できる。
option
の戻り値として、受け取った型と追加する型をマージしたものを再度 Chainable
に入力したものを用意すれば、メインの挙動としては完成する。
// メインの挙動までの実装
type Chainable<T = {}> = {
option<K extends string, V>(key: K, value: V): Chainable<T & { [k in K]: V }>
get(): T
}
制約の挙動の実装
追加済みのキーを追加しようとするとエラー
これは、単に option
の引数に制約をつければ良い。
// keyの型を "K extends keyof T ? never : K" として制約をつける
type Chainable<T = {}> = {
option<K extends string, V>(key: K extends keyof T ? never : K, value: V): Chainable<T & { [k in K]: V }>
get(): T
}
追加済みのキーを追加した時の実行結果は上書き
単純な場合の挙動から勘違いしやすいが、交差型は型を合体しているわけではない。
同じ名前のキーがあった場合は、それぞれのキーの型同士の積集合をとったものになる。
よってここまでの実装では、既存のキーに異なる型を追加しようとすると never
型になってしまう。
// ts-challengeのAlike型で試す
type Hoge = { a: number, b: string } & { a: string, c: boolean }
type result = Alike<Hoge, { a: never, b: string, c: boolean }>
// type result = true
なので、キーを追加する部分では、既存のキーを Omit
してから追加しないといけない。
Omit
部分を自前で実装する方が綺麗な型が得られるが、全てのテストケースを通すという観点ではこれがシンプルな解法だと思う。
type Chainable<T = {}> = {
option<K extends string, V>(key: K extends keyof T ? never : K, value: V): Chainable<Omit<T,K> & { [k in K]: V }>
get(): T
}
00020-medium-promise-all
TBD
00296-medium-permutation
ユニオン型の順列を生成する問題。
ユニオン型で extends
による分岐を行う場合は、結合されているそれぞれの型について型定義を計算していくので、実質forループとみなせる。
大まかな方針としては以下のような再帰で考えられる。
- ユニオン型の各型を取り出す
- その型が初めにきて、後ろがそれ以外の型の順列になるような配列を返す
- 空の配列を受け取った場合は空の配列を返す
「それ以外の型」という部分は組み込み型の Exclude
を用いることができる。
この部分で元のユニオン型とループ変数としての型が必要になるため、 T
だけでなくもう一つ変数を用意する必要がある。
type Permutation<T, K = T> = T[] extends never[]
? []
: K extends K
? [K, ...Permutation<Exclude<T, K>>]
: never
00298-medium-length-of-string
文字列の長さを取得する問題。
配列の長さはちゃんと数字が返ってくるが、string.length
はどんな場合でも number
が返ってしまう。
裏を返せば、文字列を分解して1文字ずつの配列に変換してしまえば、あとは初級問題のとおり length
プロパティを参照すればOK。
型システムの中でsplitのような関数を実行できるわけもないので、1文字ずつ再帰的に分解していく。
具体的には、未分解の文字列と分解済み文字配列の2つを受け取り、文字列から初めの1文字だけ取り出して配列に移して次に渡す……のようにする。
type LengthOfString<S extends string, T extends string[]=[]> =
S extends `${S[0]}${infer Rest}`
? LengthOfString<Rest, [...T, S[0]]>
: T['length']
00612-medium-kebabcase
文字列で infer
するときの挙動をまとめておく。
type Infer<S extends string> = S extends `${infer F}${infer R}` ? F | R : never
type Ex1 = Infer<''>
// Ex1 = never
type Ex2 = Infer<'a'>
// Ex2 = '' | 'a'
type Ex3 = Infer<'ab'>
// Ex3 = 'a' | 'b'