Open12

Type Challenges(初級編)

tac-tac-gotac-tac-go

Readonly

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

解答

実装

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

実装の考え方としてTodoのプロパティを取得し(keyof)その値に順番にアクセス(インデックスアクセス型)し、その値に対してreadonly (readonly修飾子)をつけることを問われている問題である。問題としてReadonlyを使わずに実装する形ではあるが、このような設定はよくするので実際は Readonly型が定義されている。

tac-tac-gotac-tac-go

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

lengthとextendsの使い方がわかっていれば解ける問題。タプルに対して lengthを用いることで長さが取得できる。配列リテラルは 内部的に特殊なプロパティlengthを持つインターフェイスと同様の意味を持つ。そのため、lengthを使うと値を取得することができる。
readonlyをつけることで読み込み専用の配列として読み込む。また、extendsをつけることでTを読み取り専用の配列型であると制約し、それ以外の値が代入できないようにする必要がある。
意外と落とし穴なのはリテラル型としての考え方である。最終的に T['length'] は長さを取得し、数値を代入することになりリテラル型となる。型変換をしたり、インデックスアクセスをしたりするなど何か操作をすることが多いが直接値を代入することも可能である。

tac-tac-gotac-tac-go

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 PropertyKey[]> = {
  [K in T[number]]: K;
}

as const で定義された 配列を読み込むので readonly をつけて割り当てできない配列を受け入れられるようにする。
タプルの中身を取り出して Mapped Types で順番に実行していくときは numberを使うことで、順番にプロパティにアクセス可能となる。string | number | symbol のユニオン型であるPropertyKeyをつけることでオブジェクトのキーとして使用できる値を明確にしている。

tac-tac-gotac-tac-go

Push

問題
https://github.com/type-challenges/type-challenges/blob/main/questions/03057-easy-push/README.ja.md

解答

実装

type Push<T extends readonly any[],K> = [...T,K]

非常にシンプルな実装で実装可能である。既存の配列から新しい配列を作り替えるので返り値の配列の中で既存の配列をスプレッド構文で展開し、文字列Kも加えれば新しい配列が生成される。
これに関してはスプレッド構文という JavaScript の実装さえ理解できていたら解けるのでこのチャレンジの実装のなかでは一番簡単だと思う。

tac-tac-gotac-tac-go

Pick

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

解答

実装

type MyPick<T, K extends keyof T> = {
  [P in K] : T[P]
};

一つ重要となりうるのが、 K extends keyof T の実装である。これは KがTのプロパティであるという保証をおこなっている。これがなければTがインデックスを参照できる可能性がないときもあるのでこの実装が思いつくかどうかが鍵となる。ただしこちらもよく記述する実装なので Pick は実装されており、こちらを使えば便利。

tac-tac-gotac-tac-go

If

問題
https://github.com/type-challenges/type-challenges/blob/main/questions/00268-easy-if/README.ja.md

解答

実装

type If<C extends boolean,T,F> = C extends true ? T : F

extends boolean で Cの値がbooleanであることを保証している。
Conditional Types は 三項演算子とほとんど同じ書き方であり、 extendsの後に期待する値を書き込みそれで条件分岐をおこなう。Conditional Types の書き方に悩まなければ簡単に解ける問題。

tac-tac-gotac-tac-go

Awaited

問題
https://github.com/type-challenges/type-challenges/blob/main/questions/00189-easy-awaited/README.ja.md

解答

実装

type MyAwaited<T> = T extends PromiseLike<infer P>? MyAwaited<P>: T; 

Conditional Types で TがPromise型であるということを検証している。また知りたい値の前にinferをつけることで値を推論してくれるのがinferであり、今回はPromiseが内包する型を知りたいのでその部分にinferをつけるとその部分が推論されて答えとなる。PromiseLikeをつけることでTがPromiseのような型なのかどうかを再帰的に判定している。そうじゃない場合は、これ以上再帰するわけではないのでそのままTを返せばそのままTが型となる。

tac-tac-gotac-tac-go

Concat

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

解答

実装

type Concat<T extends any[],K extends any[]> = [...T,...K];

この問題はPush、Unshiftとほとんど解法は同じである。配列が入力なのでextendsで入力が型制限されるようにする。あとはスプレッド構文で値を展開して新しい値を作ればよい。

tac-tac-gotac-tac-go

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]

inferを使う実装

type First<T extends any[]> = T extends [infer U,... any[]] ? U : never

個人的には最初の実装がいいと感じている。extendsで配列が空の状態であるかどうかを判定し、空であれば値がないことを示すneverを返している。
ただし、inferを使う実装でも可能。スプレッド構文で残りの値を全て受け取ることにして最初に値の型をinferで推論している。

tac-tac-gotac-tac-go

Includes

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

解答

実装

type Includes<T extends readonly any[], U> =
  T extends [infer P, ...infer rest]
    ? Equal<P, U> extends true
      ? true
      : Includes<rest, U>
    : false

実装としてはいくつか考えられるが、テストケースのエラーをなくすには上記のような実装が考えられる。type-challengesには Equal<X,Y> 型が用意されており、X と Y の等価判定を行う。
まず [infer P, ...infer rest] パラメータPだけを取り出し、それを求めたい値と一致するかを再帰的に繰り返し、そうであればtrueをそうでなければ最終的にはfalseを返すようにしている。

もし、このような複雑な実装になってしまっているのは入力される値が常に配列であると保証されないからだ。

type Includes<T extends readonly any[],U> = U extends T[number] ? true : false

このような実装は

Expect<Equal<Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'>, false>>,
Expect<Equal<Includes<[1, 2, 3, 5, 6, 7], 7>, true>>

といった配列にインデックスでアクセスものに関しては問題ないが、

Expect<Equal<Includes<[{}], { a: 'A' }>, false>>

のように配列でないものにアクセスをしようとするとエラーが解消されない。少し応用編のような問題だと感じる。

tac-tac-gotac-tac-go

Parameters

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

解答

実装

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

少し難しいが今までの知識の組み合わせで解ける問題である。 MyParametersにジェネリクスとして渡しているのは関数fooであり、これは引数を受け取り関数を実行するものである。そのためT自体は関数の形である必要があるのでextendsで型制限をおこなっている。今回は引数の型を知りたいので引数の型自体にinferを使う。...anyというふうに残余引数を使っているので、残余引数構文により、関数が不定数の引数を配列と取得される。そのため答えとなるKは配列となる。引数に対して型を推論して予測して返すので上記のような実装となる。