はじめに
先日、type-challenges を紹介する以下の記事を投稿しました。
上記の記事では難易度 warm-up
1問(13・Hello World)と、難易度 easy
の2問(7・Readonly、4・Pick)を解説しました。
そして、この記事では難易度 easy
の全13問(2023/03/15時点)のうち、上記を除く残りの11問をすべて解説します!
難易度 easy
残りの11問をすべて解説します!
それぞれの問題で必要な知識の都合上、以下の順番で説明します。
- 18・Length of Tuple
- 11・Tuple to Object
- 3057・Push
- 3060・Unshift
- 268・If
- 14・First of Array
- 43・Exclude
- 189・Awaited
- 533・Concat
- 898・Includes
- 3312・Parameters
18・Length of Tuple
タプルの型が与えられるので、その長さのリテラル型を返す型 Length
を作成する問題です。
type Length<T> = any
タプル型は、[string, number, boolean]
のような各要素の型や要素の数が決まっている配列の型です。配列では arr.length
のように length
で長さが取得できました。タプルも同様に length
で長さを求めることができます。タプルは要素数が決まっているので、length
の型が判明すればよさそうです。
前回の記事でも少し紹介しましたが、インデックスアクセス型でオブジェクトの型からそのプロパティの型を求めることができます。今回は T['length']
のように書くことで、タプル型から length
の型が取得できます。
type Length<T> = T['length']
そして、「T はタプル型である (= length
プロパティを持つ) 」という型引数の制約をつけたら完成です!
type Length<T extends readonly any[]> = T['length']
11・Tuple to Object
タプル型が与えられ、各要素の key と value を持つオブジェクト型を作成する TupleToObject
型を作る問題です。
type TupleToObject<T extends readonly any[]> = any
例として、['a', 'b']
というタプル型が与えられたとき、TupleToObject<['a', 'b']>
は { 'a': 'a', 'b': 'b'}
型になれば OK です。インデックス型を使って、key と value が同じようなプロパティを持つオブジェクト型を作成すればよさそうです。
type TupleToObject<T extends readonly any[]> = { [K in ????]: K }
では、タプル型から各要素の型を取得するにはどうすればよいでしょうか?配列の場合、 arr[0]
のように要素の位置を指定すればその要素を取得できました。ここで要素の位置は 0
や 1
のような number
です。前の問題ではオブジェクト型に T['length']
のようにプロパティのキーの型を指定することで、その要素の型が取得できました。これと同様に、T[number]
とすると、タプルの要素の型が取得できます。例として、T
が ['a', 'b']
の場合、 T[number]
は 'a' | 'b'
になります。
これでタプルから要素の型を取得できそうなので、それを使って以下のようにインデックス型で指定された型が作れそうです。
type TupleToObject<T extends readonly any[]> = { [K in T[number]]: K }
ですが、これだけではまだ足りません。以下のように要素が配列やオブジェクトの場合にエラーになってほしいところですが、エラーになっていません。
// @ts-expect-error
type error = TupleToObject<[[1, 2], {}]>
オブジェクトのキーは string | number | symbol
のどれかのみ可能です。そこで、その制約をつければ完成です!
type TupleToObject<T extends readonly (string | number | symbol)[]> = { [K in T[number]]: K }
3057・Push
タプル型 T
ともう一つの型 U
が与えられるので、 Array.push
のように T
の末尾に U
を追加した型を作成する問題です。
type Push<T, U> = any
型の push
を考える前に、値の push
を考えてみましょう。JavaScript および TypeScript では、スプレッド構文 を使うことで配列の展開ができました。例として arr = [1,2]
のとき、 [...arr, 3]
と書くことで [1,2,3]
を作ることができました。実は型でも同様に、[...T, U]
と書くことができます!
あとは T
に制限を付けたら完成です!!
type Push<T extends any[], U> = [...T, U]
3060・Unshift
次は Array.unshift
の型バージョンです。
type Unshift<T, U> = any
Array.push
とは逆に、先頭に U
を追加する問題ですね。これも先程と同様に、型のスプレッド構文で実装できます。制限を付けたら完成です。
type Unshift<T extends any[], U> = [U, ...T]
268・If
型引数 C
が true
型だったら T
を、 C
が false
型だったら F
を返す IF
型を作る問題です。
type If<C, T, F> = any
ついにきました!型の条件分岐です!!TypeScript では、三項演算子を使うことで型の条件分岐ができます。この機能は Conditional Typesと呼ばれています。
Conditional Types は T extends U ? X : Y
のように記述します。 T
が U
の部分型1である場合、 X
を返し、そうでないとき Y
を返します。今回の問題では、C
が true
の部分型かどうかを条件とすればよさそうです。
type If<C, T, F> = C extends true ? T : F
あとは、C
が boolean
となるように制限を追加すれば完成です。
type If<C extends boolean, T, F> = C extends true ? T : F
14・First of Array
タプル型 T
を受け取り、その最初の要素の型を返す First
を作る問題です。
type First<T extends any[]> = any
配列の最初の要素なので T[0]
としたら取得できそうです。
type First<T extends any[]> = T[0]
ですが、これだけでは足りません。テストケースには以下のように T
が []
のときに never
になってほしい様子です。
Expect<Equal<First<[]>, never>>
T
が空の場合は never
、そうでない場合は T[0]
を返せばよさそうです。前の問題と同様に、Conditional Typesで解決できます。空の配列の型は []
なので、条件は T extends []
にすればよさそうです。これで完成です!
type First<T extends any[]> = T extends [] ? never : T[0]
43・Exclude
第一型引数から、第二引数の型を除いた型を作る問題です。
例として、MyExclude<'a' | 'b' | 'c', 'a'>
は 'b' | 'c'
になればOKです。
type MyExclude<T, U> = any
この問題を解くためには、Conditional Types の Union distribution という性質を知っておく必要があります。
Union distribution とは T extends X ? Y : Z
の時に、T
がユニオン型の場合に分配法則のように展開されるという性質です。例として T = A | B
の時、以下のように展開されます。
(A | B) extends X ? Y : Z
=> (A extends X ? Y : Z) | (B extends X ? Y : Z)
今回の問題では以下のように、T
が 'a' | 'b' | 'c'
のとき、それぞれについて U
の部分型になっている場合に never
を返し、そうでないときはその型を返す、ということがしたいです。
('a' extends U ? never : 'a') | ('b' extends U ? never : 'b') | ('c' extends U ? never : 'c')
これはまさに、Union distribution によって分配されるので、以下のように書くだけで完成してしまいます!
type MyExclude<T, U> = T extends U ? never : T
189・Awaited
ユーティリティ型の Awaited
を作成する問題です。Awaited
は Promise<string>
から string
を取り出す型です。
type MyAwaited<T> = any
この問題も条件分岐で解けそうです。T
が Promise<U>
の場合、U
を返し、そうでない場合は never
を返せばよさそうです。しかし、以下のように書くことはできません。なぜなら、U
は未定義の型変数だからです。
type MyAwaited<T> = T extends Promise<U> ? U : never
このような場合に、Conditional Types のもう一つの強力な機能 infer
を使うことで解決できます。 使い方はとっても簡単で、U
の前に infer
と書くだけです。こう書くことで、U
を使うことができます。
type MyAwaited<T> = T extends Promise<infer U> ? U : never
これで完成かと思いきや、まだ足りません。3つ目のテストケースは以下のようになっています。
type Z = Promise<Promise<string | number>>
Expect<Equal<MyAwaited<Z>, string | number>>
Promise
の中にさらに Promise
がある場合、その中の型を返す必要があります。U
がまだ Promise
だった場合、MyAwaited<U>
を返し、そうでないときは U
を返すことで、再帰的に、Promise
の中の型を見つけられそうです。
type MyAwaited<T> =
T extends Promise<infer U>
? U extends Promise<any> ? MyAwaited<U> : U
: never
これで、深くネストされた Promse
の中の型を取り出すことはできましたが、まだ通らないテストケースがあります。
type T = { then: (onfulfilled: (arg: number) => any) => any }
Expect<Equal<MyAwaited<T>, number>>
T
は Promise
と同等のオブジェクトの型です。このような方も同様に扱うためには extends Promse
としているところを extends PromiseLike
に変えることで可能になります。
type MyAwaited<T> =
T extends Promise<infer U>
? U extends Promise<any> ? MyAwaited<U> : U
: never
最後にもう一つ、T
が PromseLike
ではない場合にエラーにしたいので、制約を付けたら完成です。
// @ts-expect-error
type error = MyAwaited<number>
type MyAwaited<T extends PromiseLike<any>> =
T extends PromiseLike<infer U>
? U extends PromiseLike<any> ? MyAwaited<U> : U
: never
533・Concat
Array.concat
の型バージョンを作成する問題です。
type Concat<T, U> = any
最初に T
と U
が配列になるように、制約をつけておきましょう。
type Concat<T extends any[], U extends any[]> = any
ここまでの問題で、型による条件分岐と型の再帰が出てきました。型で作る前に、値の concat
を条件分岐と再帰で作ってみます。
function concat(xs: any[], ys: any[]): any[] {
if (xs.length === 0) {
return ys
} else {
const [first, ...rest] = xs
return [first, ...concat(rest, ys)]
}
}
あとは、これを型バージョンに書き直すだけです。T extends [infer First, ...infer Rest]
で分岐することで、T
が空でない場合と空の場合で分岐できます。空でない場合、First
と Rest
を使って再帰部を作り、空の場合に U
を返したら完成です。
type Concat<T extends any[], U extends any[]> =
T extends [infer First, ...infer Rest]
? [First, ...Concat<Rest, U>]
: U
898・Includes
タプル型 T
に U
が含まれているか判定する型を作成する問題です。
type Includes<T extends readonly any[], U> = any
こちらも先程と同様に、T extends [infer First, ...infer Rest]
で分岐し、最初の要素が U
と同じか判定し、異なる場合も再帰的に判定すればよさそうです。
type Includes<T extends readonly any[], U> =
T extends [infer First, ...infer Rest]
? Equal<First, U> extends true ? true : Includes<Rest, U>
: false
型が同じか判定する Equal
は、playground で型をテストする際に使用しているものを利用しました。
import type { Equal, Expect } from '@type-challenges/utils'
3312・Parameters
最後は関数型から引数の型を得る問題です。
type MyParameters<T extends (...args: any[]) => any> = any
すでに T
に (...args: any[]) => any
という制約がついていますが、この中の args
の型を infer
で取り出すだけです。取り出せない場合は never
を返しましょう。
type MyParameters<T extends (...args: any[]) => any> =
T extends (...args: infer Args) => any
? Args
: never
おわりに
いかがでしたか?僕自身まだ理解が浅い部分もあるため、拙い解説になってしまっている部分もあるかもしれませんが、参考になったのなら幸いです。
型レベルプログラミングは制約が多く普段のプログラミングより難しいですが、テストケースを元に少しずつゴールに向かって進んでいくのが楽しかったです。知らないと解けない機能があったりするので、そこはあきらめて解答例を参考にするとよさそうです。また、型の条件分岐や infer
による推論、再帰ができるので、一旦値バージョンで作成したのち、型に変更すると作りやすいです。答えも一つではなく、別解もあるので、よりエレガントな回答を探す楽しみ方もあると思います!
次は難易度 medium の問題に挑戦しようと思います!また何問か解けたら記事にします!お楽しみに!!
-
TypeScript の部分型については 以前書いたこちらの記事 で紹介しているので、ぜひ読んでみてください! ↩