Open13

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 でない型を入力するとエラーにする必要があるので、解法としては以下のようになる。

  1. 再帰的に PromiseLike を剥がす型
  2. 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 を含んでいるかを判定する」。
アルゴリズム的なことを記述しないのでこういう書き方をしたいが、 booleanundefined などを受け取るとうまくいかない。

したがって、結局はarrayの中身を舐めていって1つずつ等価判定をしていく必要がある。
等価判定を行う型はtypescript-challengeで提供されている Equal を一旦使うとして、以下のような流れで再帰的に判定していく。

  1. 受け取ったarrayを最初の要素とそれ以外に分ける
  2. 最初の要素で等価判定して等しければ trueを返す
  3. 等しくなければ、2個目以降の要素のみを対象として1.を実行する
  4. そもそも最初の要素を取り出せない場合は 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 を自前で実装すればよい。
ロジックの理解についてはこちらの記事で解説されているのがわかりやすかった。
https://zenn.dev/yumemi_inc/articles/ff981be751d26c

ぺい / パンケーキガメぺい / パンケーキガメ

00003-medium-omit

ここから中級。Omitを自前実装する問題。
素朴に考えると「キーを舐めていって残すか除外するか判定する」というようなことを思い浮かべてしまうが、Key Remappingというものを使用すると比較的簡単に実現できる。

Key RemappingはMapped Typesのドキュメントに記載がある。
https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#key-remapping-via-as

端的にいうと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
}
ぺい / パンケーキガメぺい / パンケーキガメ

00296-medium-permutation

ユニオン型の順列を生成する問題。
ユニオン型で extends による分岐を行う場合は、結合されているそれぞれの型について型定義を計算していくので、実質forループとみなせる。
大まかな方針としては以下のような再帰で考えられる。

  1. ユニオン型の各型を取り出す
  2. その型が初めにきて、後ろがそれ以外の型の順列になるような配列を返す
  3. 空の配列を受け取った場合は空の配列を返す

「それ以外の型」という部分は組み込み型の 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。
https://zenn.dev/link/comments/2f20b230b3c36d

型システムの中で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'