type-challengesのEasy問題を二度と忘れないように自分に対しての解説を残してみる
Pick
問題
自分の回答
type MyPick<T, K extends keyof T> = {[key in K]: T[key]}
自分への解説
- 例えば以下のような Todo という interfaeceや型から、似たような型 を作るのかなという印象
- さらに言えば問題文に書いてあるように 似たような型 は 元の型の部分型 である必要がありそう
- 問題の初期段階から
type MyPick<T, K> = any
という状態になっていて、このまるで関数のように第1引数、第2引数と定義しているジェネリクスの一部分をType Parameters:と表現する模様- 与えられる2つの引数から動的に似たような型を作れるよというヒントを感じる
最初に考えた解法
// [key in K] という部分で反復処理をしているようなイメージ
// 具体的には MyPick<Todo, "title" | "completed"> のように複数のキーを受け取ることがあるから。反復処理をするようなイメージで
type MyPick<T,K> = {[key in K]: T[key]}
ここで利用している知識は以下の通り。
-
Mapped Types
- 公式にも
When you don’t want to repeat yourself, sometimes a type needs to be based on another type.
と書いているように 元ある型から似たような型を作る時に使えると覚えておけば便利そう
- 公式にも
-
TypeScript の"is"と"in"を理解する #TypeScript - Qiita
- こちらの記事の Mapped Type のように元ある型のキーを全てオプショナルにした型を生成するのにとても便利そう
とはいえ、結果的にこれだと K
の型が制限されておらず T という型のキーに存在が約束されないため以下のようなエラーが表示される
改善したもの(最終的な解法)
type MyPick<T, K extends keyof T> = {[key in K]: T[key]}
ここで使っている知識は以下の通りです。
-
型引数の制約 | TypeScript入門『サバイバルTypeScript』
-
K extends
の部分に該当
-
-
keyof型演算子 | TypeScript入門『サバイバルTypeScript』
-
keyof T
とすることで型引数として与えられたT
のキーを抽出している
-
- よって、 K は
keyof T
という型で制限しているような形にすることで型エラーを解消できた
感想
- 一問目から割と難しくないか!?
- これはチャレンジを終える頃にはとても理解が進みそうだぜ・・・!?
Readonly
問題
自分の解答
type MyReadonly<T> = {
readonly [key in keyof T]: T[key]
}
自分への解説
- これもPick の問題とかなり近しいニュアンスを感じる
- 元の型から似たような型 を作るというところがとても似ている
- 前の問題は
元の型から部分型を作る
というニュアンスだったが、今回は元の型のキーを全てリードオンリーにした型を作る
というもの
- 使った知識
- 前回と同じ知識
- 今回新しく使った知識
- interface の個々のプロパティをreadonlyにするテクニック
- Readonly | TypeScript Deep Dive 日本語版
イメージとしては以下のような手順で進める感じでした。
// まずは元の型と同じ意味を持つ型を作成
type MyReadonly<T> = {
[key in keyof T]: T[key]
}
あとは個々のプロパティをreadonlyにしてあげれば完成ですね。
type MyReadonly<T> = {
readonly [key in keyof T]: T[key]
}
感想
- 1問目で Mapped Typeを理解していたからすぐに応用できました
- TypeScript側で
ReadOnly
を用意してくれているので実戦ではこちらを使おう
Tuple to Object
問題
自分の回答
type TupleToObject<T extends readonly (string | number | symbol)[]> = {
[K in T[number]]: K
}
自分への解説
- これも前までの問題と同じようにMapped Type を利用するのかな?ということまではわかるのですが、一見どうすれば良いかわかりませんでした
- 色々と調べると先に答えっぽいものが目に入ってきたのですが、以下の
T[number]
のnumber
ってなんやねん?と思って腑に落ちていませんでした - そこで以下の記事を発見して、腑に落ちました
あぁぁぁ〜なるほど。
T[number]
という number型でアクセスできるすべてをUnionします
という説明で腑に落ちました
実際に試してみるとこんな感じに動作するのでわかりやすいです。
というところまでわかればあとは今までの知識を使うことで、以下のように回答できました。
type TupleToObject<T extends readonly (string | number | symbol)[]> = {
[K in T[number]]: K
}
First of Array
問題
自分の回答
type First<T extends any[]> = T extends [] ? never : T[0]
自分への解説
- おそらく作問者の意図通り最初は以下のように記載しました
type First<T extends any[]> = T[0]
すると以下の通り、配列の長さが0の時に意図した型(never
)を返却できていません
- そこで、条件分岐のようなものが必要だよな?ということで以下のような記事を発見しました
-
Conditional Typesというやつですね
- 名前とか知っているし、本では読んだことあってもなかなかスッと出てこないですね、TS力の不足を感じます
ここまでわかれば 長さ0の配列を受け取っている時はneverを、そうではない時にはT[0]の型になればよいな
ということで以下のような回答を記載しました
type First<T extends any[]> = T extends [] ? never : T[0]
Length of Tuple
問題
自分の回答
type Length<T extends readonly any[]> = T['length']
自分への解説
正直これは全くわからず固まってしまいました。
以下のスクラップのヒントを見て、インデックスアクセサ型について調べてから理解できました。
ようするに前回の問題で配列に対して T[0]
としたように今度は型を制限した T
に対して length
というプロパティ(データプロパティ)を読み込んであげれば良さそうです。
type Length<T extends readonly any[]> = T['length']
感想
- (解く前の感想)
- え?なにこれ?普通にJavaScriptだったら
hoge.length
みたいにすれば良いところを型でする、つまりTSのコンパイラにやらせるってこと? - な、なんだこれ、変態計算か!?
- え?なにこれ?普通にJavaScriptだったら
- (解いた後の感想)
- これはインデックスアクセス型 (indexed access types) の良い問題だ
- あと、Array: length - JavaScript | MDNもデータが型のプロパティであるという理解度の再発掘に素晴らしいな
Exclude
問題
自分の回答
type MyExclude<T, U> = T extends U ? never : T
自分への解説
-
Conditional Type
を使うっぽいよな - というところまでわかったのですが、
与えられたUnion型に対してfliterみたいに処理する方法がわからん
と悩んでいました - こちらも自力では解けずに色々と調べていると Distributive Conditional Types(ユニオン型の分配法則)というものがあることを知りました
ここで重要なのは、条件付き型がジェネリック型に適用された場合、Tがユニオン型の場合、その各要素に対して条件が個別に判定されること。これが分配法則の本質です。
- な、なるほど・・・「ジェネリック型 Tに対して Conditional Typeを適用する」 and 「TがUnion型である」である時には分配法則によりTの全ての要素に対して Conditional Typeが適用されるのか
- ということは、意図せず与えられるUnionに対してfilterみたいな処理が実施できるのか
ということを把握できれば、あとは素直に以下のように実装するだけでした。
type MyExclude<T, U> = T extends U ? never : T
以下のように利用できます。
MyExclude<'a' | 'b' | 'c', 'a'>, 'b' | 'c'>
// MyExclude<'a' | 'b' | 'c', 'a'> は "b" | "c" となる
感想
- 俺は雰囲気で型をやっている
- 分配法則知らん過ぎワロタ
Awaited(189)
自分の回答
// これが初級・・・?だと・・・_?
type MyAwaited<T extends PromiseLike<any>> =
T extends PromiseLike<infer U> ?
U extends PromiseLike<any> ?
MyAwaited<U>
: U
: never
自分への解説
この問題はとても手持ちの知識ではとけず、以下の知識が必要でした。
- infer型で「ある型の中に含まれる何かの型を取りだすのに利用できる」
- 最近のTypeScriptの型では再帰のようなものが使える
inferでPromiseにラップされた中の型を取り出す
まずinferの知識で以下のようにPromiseでラップされた中の型を取り出せます。
type MyAwaited<T extends Promise<any>> = T extends Promise<infer U> ? U : never
ただし、この状態だと Promise<string> のようなケースは意図通り string
を取り出すことができますが、 Promise<Promise<string>>
のような型を渡された場合に Promise<string>
となってしまいます。
よって以下のようなケースにおいてエラーが発生してしまいます。
type Z = Promise<Promise<string | number>>
type Z1 = Promise<Promise<Promise<string | boolean>>>
type T = { then: (onfulfilled: (arg: number) => any) => any }
type cases = [
Expect<Equal<MyAwaited<Z>, string | number>>,
Expect<Equal<MyAwaited<Z1>, string | boolean>>,
Expect<Equal<MyAwaited<T>, number>>,
]
再帰っぽい型を定義する
ということでやや複雑ですが、与えられたTの型が Promise をネストしてラップしているようなケースのために、再帰のようにしてやや複雑なConditional Type を書くことができます。
type MyAwaited<T extends PromiseLike<any>> =
T extends PromiseLike<infer U> ?
U extends PromiseLike<any> ?
MyAwaited<U>
: U
: never
これが「easy」の部類・・・?と思う気持ちは消えませんが、やっていること自体はそんなに複雑ではありません。
厳密にいうとちょっと違いますが以下のような感じです。
// 全体像はこんな感じ
// TがPromsieLikeでラップされているようなら再起処理にかけるような形
T extends PromiseLike<infer U> ? 再起処理 : never
// 再起処理の中はこんな感じ
U extends PromiseLike<any> ? MyAwatied<U> : U
// PromiseLikeでラップされているならMyAwatiedで再起処理、そうでなければ取り出したUを返してあげる
感想
- inferを知るための問題
- いつからか問題の難易度が上昇して、 再起処理の考えを知らないと難しくなっている様子
- 以前の問題だと Promiseがネストする問題ではなかった様子
- 単なる型パズル要素じゃないのがワクワクしますね?
- (あまり強くないとはいえ競技プログラミングやっておいてよかった)
If
問題
自分の回答
type If<C extends boolean, T, F> = C extends true ? T : F
自分への解説
- 一個前の MyAwaitedに比べたら非常に簡単な問題でした
- 素直に `型引数の制約 | TypeScript入門『サバイバルTypeScript』 と Conditional Types | TypeScript入門『サバイバルTypeScript』 だけで解ける問題でした
Concat
問題
自分の回答
type Concat<T extends readonly any[], U extends readonly any[]> = [...T, ...U]
自分への解説
- こちらもまず 型引数の制約 | TypeScript入門『サバイバルTypeScript』 で受け取る型引数をnarrowingしてあげると良さそう
- TypeScriptで型にもスプレッド構文使えるよ~って話 によると型に対してもスプレッド構文が使えるそうです
という情報が揃ったので、あとは素直に受け取った型引数の型を限定してあげて、スプレッド構文で一つの配列にして返してあげれば良さそうです。
type Concat<T extends readonly any[], U extends readonly any[]> = [...T, ...U]
Includes
問題
自分の回答
最終的には以下のように回答したものの、全てが腹落ちしているわけではありません。
type MyEqual<Left, Right> =
(<T>() => T extends Left ? 1 : 2) extends
(<T>() => T extends Right ? 1 : 2) ? true : false
type Includes<T extends readonly any[], U> =
// まず長さが0ならincludesできないのでfalseを返す。そういう早期returnみたいなもん
T['length'] extends 0 ? false
// ここも次に最初の要素が一致するならtrueを返す。そういう早期returnみたいなもん
: MyEqual<U, T[0]> extends true ? true
// 最後にTの残りの要素を再起で判定しよう。
: T extends [any, ...infer Rest] ? Includes<Rest, U> : never
自分への解説
この問題を解くにあたって2つの壁があった。
2つの型が等価か判定する型の実装
まず、 MyEqual
の 実装です。型引数で渡した型が一致するかどうかを判定するのですが、全く実装の意味がわかりませんでした。
// わからんすぎてワロタ
type MyEqual<Left, Right> =
(<T>() => T extends Left ? 1 : 2) extends
(<T>() => T extends Right ? 1 : 2) ? true : false
最初はなんで以下のようなTypeでいいじゃんと思ったのですが、確かに extends
だとsubtypeかどうかという判定しかできないのかな?という理解です。
type Test<T, U> = T extends U ? true : false;
// MyTest は true になってしまう。だって "string" という文字列は string のsubtypeだから。
type MyTest = Test<'string', string>;
そこで以下の記事を見てなんとなく、ふわっと理解した気になりました。
// これなら厳密に等価か判断できる
// 正直完全理解できているわけではないが以下の記事がわかりやすかった
// https://zenn.dev/yumemi_inc/articles/ff981be751d26c
// 任意の型 T に対して L と R が同じ挙動をするかどうか くらいの意味で捉えた方が幸せなのか?
type MyEqual<L, R> = (<T>() => T extends L ? 1 : 2) extends <T>() => T extends R
? 1
: 2
? true
: false;
type MyTest2 = MyEqual<'string', string>;
// これは falseになる
再帰による判定
ここまでくればまた再帰の考え方を利用することで、以下のような感じで考えることができそうです。
- 長さが0であればfalse(再帰のベースケース)
- 次に配列の最初の要素と U が等価か判定
- 等価でなければ配列の残りの要素で再起処理を呼び出す
type Includes<T extends readonly any[], U> =
// まず長さが0ならincludesできないのでfalseを返す。そういう早期returnみたいなもん
T['length'] extends 0 ? false
// ここも次に最初の要素が一致するならtrueを返す。そういう早期returnみたいなもん
: MyEqual<U, T[0]> extends true ? true
// 最後にTの残りの要素を再起で判定しよう。
: T extends [any, ...infer Rest] ? Includes<Rest, U> : never
これも再起関数の知識がないと絶対に理解できませんね。以前ちょっとだけだけど競プロやってないと本当に理解不能でした。
感想
難しすぎワロタ
Issueの中でも散々言われていることであるが、テストケースが追加されるにつれて難しくなっていった模様。
でもこれもMyAwaitedの問題のように、分解して考えれば解けなくはないですね。(ただ MyEqualの実装でも壁がある。絶対にEasyじゃあねぇなぁ・・・)
Push
問題
自分の回答
type Push<T extends readonly any[], U> = [...T, U]
自分への解説
1問前と違ってとっても素直でかわいい問題ですね。
Concat で行ったように型でもスプレット演算子が使えることを知っておけば解ける問題だと思います。
Unshift
問題
自分の回答
type Unshift<T extends readonly any[], U> = [U, ...T]
自分への解説
こちらも同様に型でもスプレット演算子が使えることを知っておけば解ける問題ですね。
2問前との難易度の落差で風邪をひきそう
Parameters
問題
自分の回答
type MyParameters<T extends (...args: any[]) => any> =
T extends ((...args: infer U) => any)
? [...U]
: never
自分への解説
以下の問題と同じで infer
により「何かにラップされた何かの型を取り出す」という知識があれば回答できます。
ここでは「関数型の中に渡された型」を使いたいので素直に上の回答のように infer で取り出してあげればOKです。