噂の【type-challenges】をやってみた
はじめに
レベルが4段階ありますが,おそらくTypeScript上級者での基準なのであまり気にせずに学習するのが良いと思います.
今回はeasy
の中から特に勉強になったものをピックアップしてやっていきます.
typescript@4.4.4
Pick
以下のMyPick
を考えていきます.
interface Todo {
title: string
description: string
completed: boolean
}
type TodoPreview = MyPick<Todo, 'title' | 'completed'>
const todo: TodoPreview = {
title: 'Clean room',
completed: false,
}
回答例は以下のようになります.
type MyPick<T extends Record<PropertyKey, any>, K extends keyof T> = {
[P in K]: T[P]
}
keyof
まずkeyof
はイメージの通り,オブジェクトからそのキーをユニオンとして取り出します.例えば以下のようになります.
type User = {
id: number
name: string
}
type Key = keyof User // 'id' | 'name'
今回の場合,Kはkeyof T
をextends
しているのでTというオブジェクトのキーのいずれかをとるということになります.
Record
そしてRecord<K, V>
は Kがプロパティの型を示し, V型のレコードを持ちます.
例えば
const user: Record<keyof User, string> = {
id: string,
name: string
}
のようになります.User
のキーを取り,レコードの型はstring
となります.よって先ほどのUser
型とは少し異なります.
in
次に[P in K]
の部分ですが,ここのin
はfor in
のような場合の使い方と似ており,先ほどKはユニオンであったのでその要素を全て反復して取り出すようなイメージです.
T[P]
最後のレコードにあたる部分ですが,これはまるでオブジェクトのプロパティにアクセスするように記述することができます.
Tuple to Object
以下のTupleToObject
を考えていきます.
const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const
type result = TupleToObject<typeof tuple> // expected { tesla: 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}
回答例は以下のようになっています.
type TupleToObject<T extends readonly string[]> = {
[P in T[number]]: P
}
先ほどのPick
でてきた構造のものがほとんどです.
T[number]
今回は反復させたいものがタプル型なのでT[Key]
のような形は使えません.
タプルの時はT[number]
とすることで反復することができます.
ただこれを使用する時はTが明示的にタプルであることが宣言されていないといけません.
Exclude
ビルトインのExclude<T, U>
を実装します.
Exclude<T, U>
はTからUを取り除く型です.
type sports = Exclude<
'baseball' | 'soccer' | 'swimming' | 'computer',
'computer'
> // "baseball" | "soccer" | "swimming"
回答例は以下のようになります.
type MyExclude<T, U> = T extends U ? never : T
分配法則
この型を理解する上で大切なのが,条件分岐をする時はそれがユニオン型の時は分配して処理を行うということです.つまりT extends U
のときTがユニオン型であればそれぞれに対してextends
であるかどうかを判断するということです.
そのため今回の場合
T extends U ? never : T
の部分でそれぞれの値,例で言うと'baseball'
や'soccer'
に対し,extends U
つまり'computer'
をextends
しているかどうかを判定すれば良いのです.
この分配についてはたくさん使用するので理解しておくととても役に立ちます.
Awaited
Promiseでラップされた型を取り出すAwaitable
typeを実装していきます.
例えばPromise<ExampleType>
を与えたとき,ExampleType
を返すようなものです.
回答例は以下のようになります.
type Awaitable<T> = T extends Promise<infer U> ? U : T
infer
この型を理解する上で一番重要なのが,infer
です.
infer
は条件分岐の際に使用でき,型を扱うことができる変数のようなものです.
T extends Promise<infer U>
の部分で,
UがPromise<string>
やPromise<number>
などのstring
やnumber
を受け取ります.
そしてその条件分岐の後半でその変数Uを使うことができます.
つまりPromiseでラップされている型の場合,中身を取り出してそれを返すようなイメージです.
このinfer
も非常によく使うので,理解しておくととても役に立ちます.
Parameters
ビルトインのParameters<T>
を実装します.
Parameter<T>
は関数型Tの引数の型をタプル型として受け取ります.
以下のMyParameters
を実装します.
const func = (plice: number, name: string) => {
console.log(`${plice} yen`)
console.log(`username: ${name}`)
}
type ParamType = MyParameters<typeof func>
回答例は以下のようになります.
type MyParameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never
// [plice: number, name: string]
...args: infer P
先ほどのAwaitedでも出てきたinfer
を使用します.
今回の場合,引数の型を全て取り出したいのでスプレッド構文のように...args
と記述します.そしてその型をinfer P
で取得すると実装することができます.
参考
Discussion