💬

ゆるふわTS勢によるtype-challengesにつまづいた人向けの解説

tekihei23172022/06/07に公開

type-challengesをやってみたのですが、TypeScriptの型の演算をほとんどやったことがなかったため、Easyの1問目のPickからつまづいてしまいました。そのため、この問題の解説を書いて理解を整理してみようと思います。

結論からいうと、TypeScript HandbookのType Manipulationのところを一通り読んでから取り組むのがオススメです。

TypeScript: Documentation - Creating Types from Types

問題文

組み込み型のPick<T, K>を実装してください。

Pick<T, K>は、オブジェクト型Tとユニオン型Kを引数にとり、TからKに含まれるプロパティだけを選んで残した型です。

interface Todo {
  title: string
  description: string
  completed: boolean
}

type TodoPreview = MyPick<Todo, 'title' | 'completed'> // { title: string; completed: boolean }

解答例

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

解説

シンプルな解答ですが、この解答を理解するためには以下の知識が必要です。

  • ジェネリクス
  • extends
  • keyof演算子
  • Mapped Types
  • Indexed Access Types

結構鬼ですね。1つずつ解説したいと思います。

ジェネリクス

ジェネリクスとは、主に静的型付け言語において、特定の型を指定せずにプログラムを書くための機能のことを言います。

TypeScriptでは型エイリアス(type)でジェネリクスを使うことができます。ジェネリクスを使った型とは、簡単にいうと引数を取る型です。

例えば、ある型Tを引数にとって、Tの配列型を作るToArray<T>は以下のように書けます。

type ToArray<T> = T[]; // この型を作る意味はほとんどないですが...

// 使用例
type StringArray = ToArray<string>; // string[]

extends

JavaScriptでは、extendsはクラスの継承を表します。

一方、TypeScriptの型の文脈では、A extends Bは、AがBに代入可能である(AがBのサブタイプである)ことを表します。

// trueになるもの
true extends boolean // trueはbooleanに代入可能
'title' extends 'title' | 'description' | 'completed' // ユニオン型にはユニオンを構成する型を代入可能
any[] extends { length: number } // 配列はlengthプロパティを持っているので代入可能

// falseになるもの
3 extends 'string'
'hoge' extends 'title' | 'description'

extendsは、ジェネリック型に制約を加えるときや、Conditional Types(条件分岐する型)の条件部分で使います。A extends Bが成り立つかどうかは、Conditional Typesを使って以下のように確認できます。

type Judge<A, B> = A extends B ? true : false;

type T1 = Judge<true, boolean>; // true
type T2 = Judge<'hoge', 'title' | 'description'> // false

keyof演算子

keyof演算子は、オブジェクト型から、オブジェクト型のプロパティ名からなるユニオンを作る演算子です。

type Todo = {
  title: string
  description: string
  completed: boolean
}

type TodoKeys = keyof Todo // 'title' | 'description' | 'completed'

Mapped Types

Mapped Typesは、ユニオン型の各要素を反復して、オブジェクト型を作る型です。

例えば、ユーザーの属性を表すUserKeys型から、UserKeys型に含まれるすべての属性がstring型であるUser型を作るには、Mapped Typesを使って以下のように書きます。

type UserKeys = 'firstName' | 'lastName' | 'email'
type User = {
  [key in UserKeys]: string
} // { firstName: string; lastName: string; email: string }

Indexed Access Types

Indexed Access Typesは、ある型のプロパティの型を取得するために使う型です。添字にユニオン型を指定した場合は、プロパティの型のユニオンが取得できます。

type Person = { age: number; name: string; alive: boolean }
type Age = Person['age'] // number
type AgeOrName = Person['age' | 'name'] // number | string

また、配列型Tに対してT[number]とすると、Tの要素の型を取得することができます。

問題に戻る

ここまで来ると、冒頭の問題の解答は、以下のように順番に組み立てていくことができます。

// ジェネリクスで引数を受け取れるようにする
type Pick<T, K> = any

// keyofとextendsで制約をつける
type Pick<T, K extends keyof T> = any

// Mapped Typesで反復する
type Pick<T, K extends keyof T> = {
  [P in K]: any
}

// Indexed Access Typesで型を取得する → 完成!
type Pick<T, K extends keyof T> = {
  [P in K]: T[P]
}

まとめ

PickはEasyの中では難しめの問題です。

Easyのほとんどの問題は、本記事で説明した知識とConditional Typesを使って解くことができるので、面白かった方は是非他の問題にも挑戦してみて下さい!

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

GitHubで編集を提案

Discussion

ログインするとコメントできます