Closed13

type-challengesのEasy問題を二度と忘れないように自分に対しての解説を残してみる

bun913bun913

Pick

問題

https://github.com/type-challenges/type-challenges/blob/main/questions/00004-easy-pick/README.ja.md

自分の回答

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]}

ここで使っている知識は以下の通りです。

感想

  • 一問目から割と難しくないか!?
  • これはチャレンジを終える頃にはとても理解が進みそうだぜ・・・!?
bun913bun913

Readonly

問題

https://github.com/type-challenges/type-challenges/blob/main/questions/00007-easy-readonly/README.ja.md

自分の解答

type MyReadonly<T> = {
  readonly [key in keyof T]: T[key]
}

自分への解説

  • これもPick の問題とかなり近しいニュアンスを感じる
    • 元の型から似たような型 を作るというところがとても似ている
    • 前の問題は 元の型から部分型を作る というニュアンスだったが、今回は 元の型のキーを全てリードオンリーにした型を作る というもの
  • 使った知識

イメージとしては以下のような手順で進める感じでした。

// まずは元の型と同じ意味を持つ型を作成
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 を用意してくれているので実戦ではこちらを使おう
bun913bun913

Tuple to Object

問題

https://github.com/type-challenges/type-challenges/blob/main/questions/00011-easy-tuple-to-object/README.ja.md

自分の回答

type TupleToObject<T extends readonly (string | number | symbol)[]> = {
  [K in T[number]]: K
}

自分への解説

  • これも前までの問題と同じようにMapped Type を利用するのかな?ということまではわかるのですが、一見どうすれば良いかわかりませんでした
  • 色々と調べると先に答えっぽいものが目に入ってきたのですが、以下の T[number]number ってなんやねん?と思って腑に落ちていませんでした
  • そこで以下の記事を発見して、腑に落ちました

https://zenn.dev/sjbworks/articles/b8bc7d7bacfd07#tuple-型や配列を-インデックスシグネチャの[number]を使って-union-型にする

あぁぁぁ〜なるほど。
T[number] という number型でアクセスできるすべてをUnionします という説明で腑に落ちました

実際に試してみるとこんな感じに動作するのでわかりやすいです。

https://stackblitz.com/edit/vitejs-vite-8gvjjx?file=src%2Fmain.ts

というところまでわかればあとは今までの知識を使うことで、以下のように回答できました。

type TupleToObject<T extends readonly (string | number | symbol)[]> = {
  [K in T[number]]: K
}
bun913bun913

First of Array

問題

https://github.com/type-challenges/type-challenges/blob/main/questions/00014-easy-first/README.ja.md

自分の回答

type First<T extends any[]> = T extends [] ? never : T[0]

自分への解説

  • おそらく作問者の意図通り最初は以下のように記載しました
type First<T extends any[]> = T[0]

すると以下の通り、配列の長さが0の時に意図した型(never)を返却できていません

  • そこで、条件分岐のようなものが必要だよな?ということで以下のような記事を発見しました

https://zenn.dev/nbr41to/articles/7d2e7c4e31c54c#conditional-typeとしてのextends

  • Conditional Typesというやつですね
    • 名前とか知っているし、本では読んだことあってもなかなかスッと出てこないですね、TS力の不足を感じます

ここまでわかれば 長さ0の配列を受け取っている時はneverを、そうではない時にはT[0]の型になればよいな ということで以下のような回答を記載しました

type First<T extends any[]> = T extends [] ? never : T[0]
bun913bun913

Length of Tuple

問題

https://github.com/type-challenges/type-challenges/blob/main/questions/00018-easy-tuple-length/README.ja.md

自分の回答

type Length<T extends readonly any[]> = T['length']

自分への解説

正直これは全くわからず固まってしまいました。

以下のスクラップのヒントを見て、インデックスアクセサ型について調べてから理解できました。

https://zenn.dev/link/comments/e7db7c9e547310

ようするに前回の問題で配列に対して T[0] としたように今度は型を制限した T に対して length というプロパティ(データプロパティ)を読み込んであげれば良さそうです。

type Length<T extends readonly any[]> = T['length']

感想

  • (解く前の感想)
    • え?なにこれ?普通にJavaScriptだったら hoge.length みたいにすれば良いところを型でする、つまりTSのコンパイラにやらせるってこと?
    • な、なんだこれ、変態計算か!?
  • (解いた後の感想)
bun913bun913

Exclude

問題

https://github.com/type-challenges/type-challenges/blob/main/questions/00043-easy-exclude/README.ja.md

自分の回答

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" となる

感想

  • 俺は雰囲気で型をやっている
  • 分配法則知らん過ぎワロタ
bun913bun913

Awaited(189)

自分の回答

// これが初級・・・?だと・・・_? 
type MyAwaited<T extends PromiseLike<any>> =
    T extends PromiseLike<infer U> ?
      U extends PromiseLike<any> ?
        MyAwaited<U>
        : U
      : never

自分への解説

この問題はとても手持ちの知識ではとけず、以下の知識が必要でした。

  • infer型で「ある型の中に含まれる何かの型を取りだすのに利用できる」

https://reosablo.hatenablog.jp/entry/2020/08/25/005957

  • 最近のTypeScriptの型では再帰のようなものが使える

inferでPromiseにラップされた中の型を取り出す

まずinferの知識で以下のようにPromiseでラップされた中の型を取り出せます。

type MyAwaited<T extends Promise<any>> = T extends Promise<infer U> ? U : never

https://zenn.dev/link/comments/d1e8609639a6ab

ただし、この状態だと 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がネストする問題ではなかった様子
    • 単なる型パズル要素じゃないのがワクワクしますね?
    • (あまり強くないとはいえ競技プログラミングやっておいてよかった)
bun913bun913

Concat

問題

https://github.com/type-challenges/type-challenges/blob/main/questions/00533-easy-concat/README.ja.md

自分の回答

type Concat<T extends readonly any[], U extends readonly any[]> = [...T, ...U]

自分への解説

という情報が揃ったので、あとは素直に受け取った型引数の型を限定してあげて、スプレッド構文で一つの配列にして返してあげれば良さそうです。

type Concat<T extends readonly any[], U extends readonly any[]> = [...T, ...U]
bun913bun913

Includes

問題

https://github.com/type-challenges/type-challenges/blob/main/questions/00898-easy-includes/README.ja.md

自分の回答

最終的には以下のように回答したものの、全てが腹落ちしているわけではありません。

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

// これなら厳密に等価か判断できる
// 正直完全理解できているわけではないが以下の記事がわかりやすかった
// 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じゃあねぇなぁ・・・)

bun913bun913

Parameters

問題

https://github.com/type-challenges/type-challenges/blob/main/questions/03312-easy-parameters/README.md

自分の回答

type MyParameters<T extends (...args: any[]) => any> =
  T extends ((...args: infer U) => any)
    ? [...U]
    : never

自分への解説

以下の問題と同じで infer により「何かにラップされた何かの型を取り出す」という知識があれば回答できます。

https://zenn.dev/link/comments/c3e5431b41d4d8

ここでは「関数型の中に渡された型」を使いたいので素直に上の回答のように infer で取り出してあげればOKです。

このスクラップは2024/03/30にクローズされました