💪

噂の【type-challenges】をやってみた

2021/12/29に公開

はじめに

レベルが4段階ありますが,おそらくTypeScript上級者での基準なのであまり気にせずに学習するのが良いと思います.
今回はeasyの中から特に勉強になったものをピックアップしてやっていきます.
https://github.com/type-challenges/type-challenges

typescript@4.4.4

Pick

以下のMyPickを考えていきます.

pick.ts
interface Todo {
  title: string
  description: string
  completed: boolean
}

type TodoPreview = MyPick<Todo, 'title' | 'completed'>

const todo: TodoPreview = {
    title: 'Clean room',
    completed: false,
}

回答例は以下のようになります.

MyPickAnswer.ts
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 TextendsしているのでTというオブジェクトのキーのいずれかをとるということになります.

Record

そしてRecord<K, V>は Kがプロパティの型を示し, V型のレコードを持ちます.
例えば

const user: Record<keyof User, string> = {
  id: string,
  name: string
}

のようになります.Userのキーを取り,レコードの型はstringとなります.よって先ほどのUser型とは少し異なります.

in

次に[P in K]の部分ですが,ここのinfor inのような場合の使い方と似ており,先ほどKはユニオンであったのでその要素を全て反復して取り出すようなイメージです.

T[P]

最後のレコードにあたる部分ですが,これはまるでオブジェクトのプロパティにアクセスするように記述することができます.

Tuple to Object

以下のTupleToObjectを考えていきます.

tupleToObject.ts
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'}

回答例は以下のようになっています.

tupleToObjectAnswer.ts
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を取り除く型です.

exclude.ts
type sports = Exclude<
  'baseball' | 'soccer' | 'swimming' | 'computer',
  'computer'
>  // "baseball" | "soccer" | "swimming"

回答例は以下のようになります.

ExcludeAnswer.ts
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でラップされた型を取り出すAwaitabletypeを実装していきます.
例えばPromise<ExampleType>を与えたとき,ExampleTypeを返すようなものです.

回答例は以下のようになります.

awaitableAnswer.ts
type Awaitable<T> = T extends Promise<infer U> ? U : T

infer

この型を理解する上で一番重要なのが,inferです.
inferは条件分岐の際に使用でき,型を扱うことができる変数のようなものです.

T extends Promise<infer U>

の部分で,
UPromise<string>Promise<number>などのstringnumberを受け取ります.
そしてその条件分岐の後半でその変数Uを使うことができます.
つまりPromiseでラップされている型の場合,中身を取り出してそれを返すようなイメージです.

このinferも非常によく使うので,理解しておくととても役に立ちます.

Parameters

ビルトインのParameters<T>を実装します.
Parameter<T>は関数型Tの引数の型をタプル型として受け取ります.
以下のMyParametersを実装します.

parameters.ts
const func = (plice: number, name: string) => {
  console.log(`${plice} yen`)
  console.log(`username: ${name}`)
}
type ParamType = MyParameters<typeof func>

回答例は以下のようになります.

myParameters.ts
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で取得すると実装することができます.

参考

https://qiita.com/k-penguin-sato/items/e2791d7a57e96f6144e5

Discussion