TypeScript初心者がtype-challengesに挑戦してみた

5 min読了の目安(約4600字TECH技術記事

はじめに

こんにちは。高校2年の樅山です。
2020/11 から始まった、ものづくりをする高校生のための新しいグループ、「Palettte」が主催する Palettte Advent Calendar 2020 の8日目の記事となります。

皆さんは type-challenges をご存知ですか?
私は、Web開発に型を最低限付ける程度にしかTypeScriptを使ってきませんでしたが、複雑なデータを示す型を作り出せる開発者に憧れるようになったので、以前Twitterで話題になった「TypeScriptの型を使った問題」に挑戦していきたいと思います。

事前に私がこのパズルを解く前に知っていた型の知識としては、

  • 文字型, 文字列型, 整数型, 浮動小数点型, 真偽値型などの基本的な型
  • Array, Objectなどの基本的なデータ構造の型
  • Readonly
  • Union Type
  • Distinct

程度です。

この記事には type-challenges の解答例、ネタバレが多く含まれています。
また、解いている過程をそのまま記事にしたので、TypeScriptの型について明快にまとめた記事でもありません。

type-challenges

Hello World

まず、どのように解くのかを示しているチュートリアルです。

問題
type HelloWorld = any
テストケース
import { Equal, Expect, NotAny } from '@type-challenges/utils'

type cases = [
  Expect<NotAny<HelloWorld>>,
  Expect<Equal<HelloWorld, string>>
]

HelloWorld型は、テストケースを確認するとany型ではなく、かつHelloWorld型とstring型が等しくなければならないとわかります。
ですので、

解答
type HelloWorld = string

このように変更することでテストケースをパスすることができます。

Pick<T, K>

組み込みの型ユーティリティPick<T, K>を使用せず、TからKのプロパティを抽出する型を実装します。
例えば

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

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

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

そもそも、組み込みの型ユーティリティPick<T, K>がなんのことだかわかりません。
コードを見たりすると、Pick<T, K>は、T型のプロパティKを抽出した型を作り出すようです(そのまま問題文にも書いてありました)。

調べると、mapped typeunion型を分配することができる機能を発見しました。

union型の分配
{[P in keyof T]: X}

これは、type T = T1 | T2 | T3 | T4 | T5のようなunion型を、for in文のように取り出し、Xで一つ一つの型に適用し、それをunion型にした型を作れるようです。
そこで、

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

と書くと、Type 'P' cannot be used to index type 'T'.と怒られてしまいました。
PプロパティはT型のインデックスには使えないよ、ということですが、そもそもKT型のプロパティじゃなかったのでしょうか。

調べると、

そもそもKT型のプロパティじゃなかったのでしょうか。

これを明示しなければいけなかったことに気付きます。

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

とすることで、K型はT型のプロパティのunion型だと明示できるため、テストケースにパスすることができます。

Readonly<T>

組み込みの型ユーティリティ Readonly<T> を使用せず、T のすべてのプロパティを読み取り専用にする型を実装します。実装された型のプロパティは再割り当てできません。
例えば

interface Todo {
  title: string
  description: string
}

const todo: MyReadonly<Todo> = {
  title: "Hey",
  description: "foobar"
}

todo.title = "Hello" // Error: cannot reassign a readonly property
todo.description = "barFoo" // Error: cannot reassign a readonly property

なんとなく、また同じようにT型のパラメータを分配してreadonlyにすればいいと方針が立ったので実装します。

解答
type MyReadonly<T> = {
  readonly [P in keyof T]: T[P]
}

Tuple to Object

タプルを受け取り、その各値のkey/valueを持つオブジェクトの型に変換する型を実装します。
例えば

const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const

const result: TupleToObject<typeof tuple> // expected { tesla: 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}

配列を一つずつ取り出して、keyとvalueにどちらも同じ値を持つオブジェクトを取ればいいのですが、あまり思いつきません。

解答をいろいろ見ると、配列はkeyに自然数をとるオブジェクトと考えることもできるので、

type TupleToObject<T extends readonly any[]> = {
  [P in T[number]]: P
}

と書けることが分かりました。

First of Array

配列 T を受け取り、その最初のプロパティの型を返す First<T> を実装します。
例えば

type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]

type head1 = First<arr1> // expected to be 'a'
type head2 = First<arr2> // expected to be 3

直感的には、Tuple to Objectで解いたように、T[number]を列挙して一番目を返せばいいと考えていました。
しかし、型定義で具体的な値を用いることはできないようでした。
ウンウン悩んだ末に、解答を見ました。

解答
type First<T extends any[]> = T extends [infer X, ...infer rest] ? X : never

????????????????
全く分かりません。
Lispでcarcdrを取るような処理がされていそうです。

調べると、infer型変数という、型定義内で利用できる一時的な変数と分かりました。
つまりリストの先頭と残りに分け、XtruthyであればXを、falsyであればneverを返すのでしょう。

型だけで(しかも初級編の知識で)ここまで複雑な表現ができることに驚きました。

Length of Tuple

タプル T を受け取り、そのタプルの長さを返す型 Length<T> を実装します。
例えば

type tesla = ['tesla', 'model 3', 'model X', 'model Y']
type spaceX = ['FALCON 9', 'FALCON HEAVY', 'DRAGON', 'STARSHIP', 'HUMAN SPACEFLIGHT']

type teslaLength = Length<tesla>  // expected 4
type spaceXLength = Length<spaceX> // expected 5

直感的には、Xrestに分け、一時的な変数を使いまわしてrestrestを再帰的に取り続ければ実装できそうです。
しかし、調べると具体的な値を使いまわしたり、再帰的な型を作ることは難しそうです。
ウンウン悩んだ末に、解答を見ました(難しい...)

解答
type Length<T extends { length: number }> = T['length']

なるほど、確かに配列にはlengthというプロパティがあり、それを参照すれば一発で取得できます。
かなり驚きました。

終わりに

TypeScriptを今まで雰囲気で使っていましたが、型をきちんと理解して使えるようになるまではとても膨大な時間がかかりそうに思えました。
今回取り組んだ問題は全て初級編ですが、ほとんど自力で解くことはできませんでした。
チャレンジを一度中断して、型についてもう一度勉強してから再チャレンジしてみたいと思います。