LoginSignup
15
4

More than 1 year has passed since last update.

【TypeScript】type-challenges の 難易度 easy 全問解いてみた!【解説つき】

Posted at

はじめに

先日、type-challenges を紹介する以下の記事を投稿しました。

上記の記事では難易度 warm-up 1問(13・Hello World)と、難易度 easy の2問(7・Readonly、4・Pick)を解説しました。

そして、この記事では難易度 easy の全13問(2023/03/15時点)のうち、上記を除く残りの11問をすべて解説します!

image.png

難易度 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] のように要素の位置を指定すればその要素を取得できました。ここで要素の位置は 01 のような 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

型引数 Ctrue 型だったら T を、 Cfalse 型だったら F を返す IF 型を作る問題です。

type If<C, T, F> = any

ついにきました!型の条件分岐です!!TypeScript では、三項演算子を使うことで型の条件分岐ができます。この機能は Conditional Typesと呼ばれています。

Conditional Types は T extends U ? X : Y のように記述します。 TU の部分型1である場合、 X を返し、そうでないとき Y を返します。今回の問題では、Ctrue の部分型かどうかを条件とすればよさそうです。

type If<C, T, F> = C extends true ? T : F

あとは、Cboolean となるように制限を追加すれば完成です。

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 を作成する問題です。AwaitedPromise<string> から string を取り出す型です。

type MyAwaited<T> = any

この問題も条件分岐で解けそうです。TPromise<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>>

TPromise と同等のオブジェクトの型です。このような方も同様に扱うためには extends Promse としているところを extends PromiseLike に変えることで可能になります。

type MyAwaited<T> =
  T extends Promise<infer U>
    ? U extends Promise<any> ?  MyAwaited<U> : U
    : never

最後にもう一つ、TPromseLike ではない場合にエラーにしたいので、制約を付けたら完成です。

// @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

最初に TU が配列になるように、制約をつけておきましょう。

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 が空でない場合と空の場合で分岐できます。空でない場合、FirstRest を使って再帰部を作り、空の場合に U を返したら完成です。

type Concat<T extends any[], U extends any[]> =
  T extends [infer First, ...infer Rest]
    ? [First, ...Concat<Rest, U>]
    : U

898・Includes

タプル型 TU が含まれているか判定する型を作成する問題です。

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 の問題に挑戦しようと思います!また何問か解けたら記事にします!お楽しみに!!

  1. TypeScript の部分型については 以前書いたこちらの記事 で紹介しているので、ぜひ読んでみてください!

15
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
4