Open13

Type Challengesを解いていく(中級)

Yuki TerashimaYuki Terashima

Omit

自分の解答

かなり混乱した。以前解いた ExcludePick の知識が求められ、総合力を鍛えられた気がする。
結局、ここで自分は Exclude の理解がまだ甘いことを再認識できた。
最終的に解答は以下のようになった。

type MyExclude<U, T> = U extends T ? never : U;
type MyOmit<T, K extends keyof T> = { [P in MyExclude<keyof T, K>]: T[P] };

asを使った書き方もあるようで、以下が参考になった。
type-challenges をやる

Yuki TerashimaYuki Terashima

Readonly 2

自分の解答
type MyReadonly<T> = { readonly [K in keyof T]: T[K] }
type MyReadonly2<T, K extends keyof T = keyof T> = MyReadonly<Pick<T, K>> & Omit<T, K>

Pickで指定したものだけReadonlyにして、PickしていないものをOmitで補うという発想。

Yuki TerashimaYuki Terashima

Deep Readonly

自分の解答
type DeepReadonly<T> = { readonly [K in keyof T]: T[K] extends object
  ? DeepReadonly<T[K]>
  : T[K]
};

だとダメだった。

この解答を拡張させるなら、以下の解答が良さそう。

type DeepReadonly<T> = {
  readonly [K in keyof T] : T[K] extends Record<string,unknown> | Array<unknown> ?  DeepReadonly<T[K]> : T[K];
}

Issues · type-challenges/type-challenges

上記の解答でRecord<string, any>だとダメでRecord<string, unknown>だと OK なのはなぜなのかはあまりよくわかっていない・・・。

また、他の解答として

type DeepReadonly<T> = {
  readonly [k in keyof T] : keyof T[k] extends never ? T[k] : DeepReadonly<T[k]>
}

のようにキーがあるかどうかの判定もわかりやすいと思った。

9 - Deep Readonly · Issue #10261 · type-challenges/type-challenges

Yuki TerashimaYuki Terashima

Chainable Options

自分の解答

ぶっちゃけ意味不明だった。ここまで来ると黒魔術感がすごい。keyにジェネリクスを使うことと、optionが値を返す際にkeyをどこかに追加していくことは何となく分かったが、具体的な解法は思いつかなかった。

実装の手順は Chainable Options が参考になった。

type Chainable<O = {}> = {
  option<K extends string, V>(key: K, value: V): Chainable<O & { [P in K]: V }>;
  get(): O;
};

これは上記記事による最終的なコードだが、これだとkeyの重複チェックができていないため、12 - Chainable Options - type-challenges/type-challenges を参考にさらに書き換えてみた。

type Chainable<O = {}> = {
  option<K extends string, V>(key: K extends keyof O ? never : K, value: V): Chainable<O & { [P in K]: V }>;
  get(): O;
};

もちろん、以下のコードも動作する。

type Chainable<T extends object = {}> = {
  option<K extends keyof any, V>(key: K extends keyof T ? never : K, value: V): Chainable<T & Record<K, V>>
  get(): T
}
Yuki TerashimaYuki Terashima

Last of Array

自分の解答
type Last<T extends any[]> = T extends [...T, infer U] ? U : never

のような雰囲気かと思ったが、ちょっと違っていた・・・(上記は動作しない)。

type Last<T extends any[]> = T extends [...infer V, infer R] ? R : never;

15 - Last of Array · Issue #10687 · type-challenges/type-challenges

type Last<T extends any[]> = T extends [...any, infer K] ? K : never

15 - Last of Array · Issue #10415 · type-challenges/type-challenges

上記の...infer Vおよび...anyは覚えておきたい。

今回のケースだと...infer VVは使っていないので、_Vとするほうが適切かもしれない。
...anyは条件を厳しくしたい場合は...string[]のような書き方もいける。また、[...any]any[]は等しい。

Yuki TerashimaYuki Terashima

Pop

自分の解答
type Pop<T extends any[]> = T extends [...infer P, any] ? P : never;

前回の問題と似ていたため、すぐに終わりました。

Yuki TerashimaYuki Terashima

Promise.all

自分の解答
declare function PromiseAll<T>(values: T): Promise<T>;

のように、そんなシンプルにはいかなかった。


解答例は以下の通りで、as constに対応するためのreadonly [...T]や配列に対しての[K in keyof T](Mapped Types)、およびPromise<infer R>でPromiseの型を取得するのがキモだと感じた。

  declare function PromiseAll<T extends any[]>(
    values: readonly [...T]
  ): Promise<{ [K in keyof T]: T[K] extends Promise<infer R> ? R : T[K] }>;

https://github.com/type-challenges/type-challenges/blob/main/questions/00020-medium-promise-all/README.ja.md

Yuki TerashimaYuki Terashima

Type Lookup

自分の解答
type LookUp<U extends Record<'type', unknown>, T extends U[keyof U]> = 
  T extends U['type'] ? U : never;

のような雰囲気かと思ったが、全然違っていた...。


type LookUp<U, T> = U extends {type: T} ? U : never;

https://github.com/type-challenges/type-challenges/issues/10690

が解答案で、めちゃくちゃシンプルだと思った。自分は文字列どうしを比較しようとしていたが、確かにオブジェクトの比較のほうがすっきり記述できそうだと感じた。

ただ、似たような理論で文字列の比較の

type LookUp<U extends { type: string }, T> = U['type'] extends T ? U : never;

が動作しないのはなんでだろう?という気がするが...。

Yuki TerashimaYuki Terashima

Trim Left

自分の解答

そもそもTemplate Literal Typesに対するinferの使い方を知らなかったため手探りだったが、以下のようにすれば最初の一文字だけ空白のケースに対応できることはわかった。

type TrimLeft<S extends string> = S extends `${infer S1}${infer S2}`
  ?
    S2
  : never
  ;

あとはループしていけばいけるのかな...と思ったが、ループする方法がわからず断念。


以下が解答例で、なるほど確かに空白を渡せばよかったのか...と納得した。

type Space = ' ' | '\n' | '\t';
type TrimLeft<S extends string> = S extends `${Space}${infer Rest}` ? TrimLeft<Rest> : S;

https://github.com/type-challenges/type-challenges/issues/10691

今回はテストケースに'\n'なども含まれるため、Spaceとして対応する必要がある。

Yuki TerashimaYuki Terashima

Trim

自分の解答

おそらくTrim Leftの完全版?が来ると思っていたが、やはり来た。
以下のように、左側の空白を取り除いた後に右側を取り除くというロジックで成功した。

type Trim<S extends string> = S extends `${Space}${infer V}` ? Trim<V> :
  S extends `${infer U}${Space}` ? Trim<U> : S;

ただ、解答例には

type Space = ' ' | '\n' | '\t';
type Trim<S extends string> = S extends `${Space}${infer Rest}` | `${infer Rest}${Space}` ? Trim<Rest> : S;

https://github.com/type-challenges/type-challenges/issues/10692

のようなものもあり、たしかにこの方がシンプルで良いな、と思った。