Type Challenges(初級編)
Readonly
問題
解答
- 必要となる知識(Easy問題のみ添付)
実装
type MyReadonly<T> = {
readonly [k in keyof T] : T[k]
}
実装の考え方としてTodo
のプロパティを取得し(keyof)その値に順番にアクセス(インデックスアクセス型)し、その値に対してreadonly
(readonly修飾子)をつけることを問われている問題である。問題としてReadonlyを使わずに実装する形ではあるが、このような設定はよくするので実際は Readonly型が定義されている。
Length of Tuple
問題
解答
- 必要となる知識(Easy問題のみ添付)
- ジェネリクス
- lengthの使い方
- リテラル型
- extendsによる型制約
実装
type Length<T extends readonly any[]> = T['length']
lengthとextendsの使い方がわかっていれば解ける問題。タプルに対して length
を用いることで長さが取得できる。配列リテラルは 内部的に特殊なプロパティlengthを持つインターフェイスと同様の意味を持つ。そのため、lengthを使うと値を取得することができる。
readonlyをつけることで読み込み専用の配列として読み込む。また、extendsをつけることでTを読み取り専用の配列型であると制約し、それ以外の値が代入できないようにする必要がある。
意外と落とし穴なのはリテラル型としての考え方である。最終的に T['length']
は長さを取得し、数値を代入することになりリテラル型となる。型変換をしたり、インデックスアクセスをしたりするなど何か操作をすることが多いが直接値を代入することも可能である。
Tuple to Object
問題
解答
- 必要となる知識(Easy問題のみ添付)
実装
type TupleToObject<T extends readonly PropertyKey[]> = {
[K in T[number]]: K;
}
as const
で定義された 配列を読み込むので readonly
をつけて割り当てできない配列を受け入れられるようにする。
タプルの中身を取り出して Mapped Types
で順番に実行していくときは number
を使うことで、順番にプロパティにアクセス可能となる。string | number | symbol のユニオン型であるPropertyKeyをつけることでオブジェクトのキーとして使用できる値を明確にしている。
Push
問題
解答
- 必要となる知識(Easy問題のみ添付)
実装
type Push<T extends readonly any[],K> = [...T,K]
非常にシンプルな実装で実装可能である。既存の配列から新しい配列を作り替えるので返り値の配列の中で既存の配列をスプレッド構文で展開し、文字列Kも加えれば新しい配列が生成される。
これに関してはスプレッド構文という JavaScript
の実装さえ理解できていたら解けるのでこのチャレンジの実装のなかでは一番簡単だと思う。
Unshift
問題
解答
- 必要となる知識(Easy問題のみ添付)
実装
type Unshift<T extends readonly any[],K> = [K,...T]
この問題は上のPushの逆をしているだけなので非常に簡単な問題である。
Pick
問題
解答
- 必要となる知識(Easy問題のみ添付)
実装
type MyPick<T, K extends keyof T> = {
[P in K] : T[P]
};
一つ重要となりうるのが、 K extends keyof T
の実装である。これは KがTのプロパティであるという保証をおこなっている。これがなければTがインデックスを参照できる可能性がないときもあるのでこの実装が思いつくかどうかが鍵となる。ただしこちらもよく記述する実装なので Pick は実装されており、こちらを使えば便利。
If
問題
解答
- 必要となる知識(Easy問題のみ添付)
実装
type If<C extends boolean,T,F> = C extends true ? T : F
extends boolean
で Cの値がbooleanであることを保証している。
Conditional Types
は 三項演算子とほとんど同じ書き方であり、 extendsの後に期待する値を書き込みそれで条件分岐をおこなう。Conditional Types
の書き方に悩まなければ簡単に解ける問題。
Awaited
問題
解答
- 必要となる知識(Easy問題のみ添付)
実装
type MyAwaited<T> = T extends PromiseLike<infer P>? MyAwaited<P>: T;
Conditional Types
で TがPromise型であるということを検証している。また知りたい値の前にinferをつけることで値を推論してくれるのがinferであり、今回はPromiseが内包する型を知りたいのでその部分にinferをつけるとその部分が推論されて答えとなる。PromiseLikeをつけることでTがPromiseのような型なのかどうかを再帰的に判定している。そうじゃない場合は、これ以上再帰するわけではないのでそのままTを返せばそのままTが型となる。
First of Array
問題
解答
実装
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で推論している。
Includes
問題
解答
- 必要となる知識(Easy問題のみ添付)
実装
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>>
のように配列でないものにアクセスをしようとするとエラーが解消されない。少し応用編のような問題だと感じる。
Parameters
問題
解答
- 必要となる知識(Easy問題のみ添付)
実装
type MyParameters<T extends (...args: any[]) => void> = T extends (...any: infer K) => void ? K : any
少し難しいが今までの知識の組み合わせで解ける問題である。 MyParametersにジェネリクスとして渡しているのは関数fooであり、これは引数を受け取り関数を実行するものである。そのためT自体は関数の形である必要があるのでextendsで型制限をおこなっている。今回は引数の型を知りたいので引数の型自体にinferを使う。...anyというふうに残余引数を使っているので、残余引数構文により、関数が不定数の引数を配列と取得される。そのため答えとなるKは配列となる。引数に対して型を推論して予測して返すので上記のような実装となる。