💥

【お試し・初級】TypeScriptのtype-challengesをやってみた

に公開

🏁 概要

TypeScriptの型の概念を勉強するために型パズル、type-challengeをやったときのメモ。

type-challengesとは型定義についてanyとなっているところを書き直してテストケースが通るようにしようね、という問題集。
テストケースはExpect<Equal<A, B>>となっておりAとBが一致するように書いてあげる。
やった時時点の問題一覧はこんな感じ。

中級以降は問題が格段に多くなるため、本記事では「お試し」と「初級」にある問題について記載。

🪢 知っておくべき知識

各問題を特にあたって知っておかないと🤯ってなりそうな知識を記載。
問題の解説を見ても分からなかったらここに戻ってきてみようの区画。

型名

ユーティリティ型

その他

🧪 問題解説 - お試し

Hello World

// expected to be string
type HelloWorld = any

// you should make this work
type test = Expect<Equal<HelloWorld, string>>

となっているので

type HelloWorld = string

としてあげればOK。

🔰 問題解説 - 初級

Pick

type MyPick<T, K> = any

type cases = [
  Expect<Equal<Expected1, MyPick<Todo, 'title'>>>,
  Expect<Equal<Expected2, MyPick<Todo, 'title' | 'completed'>>>,
  MyPick<Todo, 'title' | 'completed' | 'invalid'>,
]

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

interface Expected1 {
  title: string
}

interface Expected2 {
  title: string
  completed: boolean
}

となっている。
TypeScriptのPickを使っている書き方になるが、それを別の書き方で書いてねという問題。

type MyPick<T, K> = { K: T[K] } でいけるかと思ったがそうなると MyPick<Todo, 'title'> の時に {K: string} となるので違いそう。

じゃあ type MyPick<T, K> = { [K]: T[K] } と書いてみるが A computed property name must be of type 'string', 'number', 'symbol', or 'any'. エラーが発生。

かつ MyPick<Todo, 'title' | 'completed'> の場合に対応できなくなってしまう。

ので反復するように

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

としてみる。が Type 'K' is not assignable to type 'string | number | symbol'. となってしまう。
なので、KがTのプロパティであることを教えてあげるために

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

としてOK。

Readonly

type MyReadonly<T> = any

type cases = [
  Expect<Equal<MyReadonly<Todo1>, Readonly<Todo1>>>,
]

interface Todo1 {
  title: string
  description: string
  completed: boolean
  meta: {
    author: string
  }
}

となっている。
Readonly<T>を使用せずMyReadonlyを使ってねというもの。

type MyReadonly<T> = {
  readonly title: string
  readonly description: string
  readonly completed: boolean
  readonly meta: {
    author: string
  }
}

みたいに書いてもエラーは起きないが、 T を使ってないので利用してあげると以下の書き方になる。

type MyReadonly<T> = {
  readonly [key in keyof T]: T[key]
}

Tuple to Object

type TupleToObject<T extends readonly any[]> = any

const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const
const tupleNumber = [1, 2, 3, 4] as const
const sym1 = Symbol(1)
const sym2 = Symbol(2)
const tupleSymbol = [sym1, sym2] as const
const tupleMix = [1, '2', 3, '4', sym1] as const

type cases = [
  Expect<Equal<TupleToObject<typeof tuple>, { 'tesla': 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y' }>>,
  Expect<Equal<TupleToObject<typeof tupleNumber>, { 1: 1, 2: 2, 3: 3, 4: 4 }>>,
  Expect<Equal<TupleToObject<typeof tupleSymbol>, { [sym1]: typeof sym1, [sym2]: typeof sym2 }>>,
  Expect<Equal<TupleToObject<typeof tupleMix>, { 1: 1, '2': '2', 3: 3, '4': '4', [sym1]: typeof sym1 }>>,

とのこと。
タプルを受け取ったら、その各値をkeyとvalueに入れたオブジェクトにするというらしい。型も保持したまま。
タプル[1, "ok", true] みたいに異なる型を要素に持つ配列のイメージ。

初めはさっきと同様に

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

と思っていたがうまくいかず。
inの右にはどうやらUnion型しか書けないみたいで T のままだと配列がそのまま入ってしまう。
それを

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

とすることで TupleToObject<typeof tuple> の場合に T[number]'tesla' | 'model 3' | 'model X' | 'model Y' になるのでOK。

First of Array

type First<T extends any[]> = any

type cases = [
  Expect<Equal<First<[3, 2, 1]>, 3>>,
  Expect<Equal<First<[() => 123, { a: string }]>, () => 123>>,
  Expect<Equal<First<[]>, never>>,
  Expect<Equal<First<[undefined]>, undefined>>,

とのこと。First<ここ>が配列の場合に、その1つ目の要素を返すイメージ。

単純に type First<T extends any[]> = T[0] にしようと思ったが First<[]> の場合にneverにならない。
それで type First<T extends any[]> = T[0] ?? never としてみても構文エラーになっていそう。

調べてみると、Typeにおける比較は extends を用いたものであるっぽい(参考)。

なので以下のようにしてあげると良い。

type First<T extends any[]> = T extends [] ? never : T[0]

ちなみに、

type First<T extends any[]> = T extends [infer R, ...any[]] ? R : never

でも問題なく通りそう。
inferは型を推測するもので、Tが 1つ目の要素の型を推論でき、かつ他に0個以上の要素を持つ配列 であれば1つ目の要素の型を表示する。
Tが空配列の場合は1つ目の要素の型推論ができないのでTは上記の条件に当てはまらないのでneverを表示するという感じ。

ちなみに、

const hoge = [3, 2, 1]
type cases = [
  Expect<Equal<First<[3, 2, 1]>, 3>>,
  Expect<Equal<First<typeof hoge>, number>>,
]

を通したい場合は type First<T extends any[]> = T extends [] ? never : T[0] にした方が最適。

type First<T extends any[]> = T extends [infer R, ...any[]] ? R : never

const hoge = [3, 2, 1]
type cases = [
  Expect<Equal<First<[3, 2, 1]>, 3>>,
  Expect<Equal<First<typeof hoge>, number>>,
]

の場合に typeof hoge が number[] になるが、要素数が0なのか1以上なのか分からないので infer R で型推論が上手にできず T extends [infer R, ...any[]] がfalseになってしまうらしい。

さらにちなみに Expect<Equal<First<hoge>, number>>, ではなく Expect<Equal<First<typeof hoge>, number>>, と書くのは
type First<T extends any[]> = T extends [] ? never : T[0] のTには値ではなく型を入れてほしいから。

なので

type cases = [
  Expect<Equal<First<[3, 2, 1]>, 3>>,
  Expect<Equal<First<typeof hoge>, number>>,
]

[3, 2, 1] も値ではなくリテラル型が入っている。
Tにはanyに含まれている配列型を入れてね、という意味。

hogeだと値が入ってしまうのでhogeの型である typeof hoge を使用してあげることが正。

Length of Tuple

type Length<T> = any

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

type cases = [
  Expect<Equal<Length<typeof tesla>, 4>>,
  Expect<Equal<Length<typeof spaceX>, 5>>,
]

とのこと。タプルの型を渡したらその長さを返してくれるLengthを作る。
T.length という書き方は使用できないが、型に対しては T['length'] を使用できるので

type Length<T> = T['length']

としてあげたいが、Tが何か分からなすぎてlengthを判定できるか分からないので

type Length<T extends string[]> = T['length']

としてあげたい。しかし、今回の定義が const tesla = ['tesla', 'model 3', 'model X', 'model Y'] as const のようにreadonlyになっているのでテストが通らないため、Tに対してreadonlyを許容するため

type Length<T extends readonly string[]> = T['length']

としてあげるとOK。

Exclude

type MyExclude<T, U> = any

type cases = [
  Expect<Equal<MyExclude<'a' | 'b' | 'c', 'a'>, 'b' | 'c'>>,
  Expect<Equal<MyExclude<'a' | 'b' | 'c', 'a' | 'b'>, 'c'>>,
  Expect<Equal<MyExclude<string | number | (() => void), Function>, string | number>>,
]

というもの。
Excludeを使用せずMyExcludeを使ってねという感じ。
extendsの処理内容を確認すると A extends B においてAがユニオン型の場合はユニオン型のそれぞれをAに入れてextends判定をするっぽい。
なので

type MyExclude<T, U> = T extends U ? [当てはまる場合] : [当てはまらない場合]
MyExclude<'a' | 'b' | 'c', 'a'>

としたら'a' extends 'a''b' extends 'a''c' extends 'a'の順に評価してくれるみたい。
それを使ってfalseの時だけ抜き出してあげればいいので

type MyExclude<T, U> = T extends U ? never : T

としてあげるとOK。

Awaited

type MyAwaited<T> = any

type X = Promise<string>
type Y = Promise<{ field: number }>
type Z = Promise<Promise<string | number>>
type Z1 = Promise<Promise<Promise<string | boolean>>>
type T = { then: (onfulfilled: (arg: number) => any) => any }

type cases = [
  Expect<Equal<MyAwaited<X>, string>>,
  Expect<Equal<MyAwaited<Y>, { field: number }>>,
  Expect<Equal<MyAwaited<Z>, string | number>>,
  Expect<Equal<MyAwaited<Z1>, string | boolean>>,
  Expect<Equal<MyAwaited<T>, number>>,
]

というもの。
Promise<string> が渡されたら string を返すMyAwaitedを作ってねらしい。
記載してないけどAwaitedは使用せず書いてねという形だと思う。
上記に実装が書いてあるのでそれをコピペしたらそりゃいい感じに通るが、一旦それは忘れて今回のテストケースに沿って書いてみる。

先述の通り渡される型が Promise<A> だったら A を返せばいいので

type MyAwaited<T> = T extends Promise<A> ? A : never

としたいが、Aってなんやねんと言われてしまう。
なので推論してあげて

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

としてあげる。

で、今回はPromiseでネストされていたらそれを全て剥がした状態にしてあげたいので場合によっては MyAwaited<>を繰り返し使用する場合がある。
それを考慮すると

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

にする。
すると後は type T = { then: (onfulfilled: (arg: number) => any) => any } の時にnumberを返してあげる。
初めのPromiseの条件には入らないので T extends Promise<infer A> でfalseの方で書いてあげると

type MyAwaited<T> = T extends Promise<infer A> 
  ? A extends Promise<any>
    ? MyAwaited<A>
    : A 
  : T extends { then: (onfulfilled: (arg: infer C) => any) => any }
    ? C
    : never;

になってOK。

ちなみに、PromiseLikeなるものが存在しており、type T = { then: (onfulfilled: (arg: number) => any) => any } の形が PromiseLike<> に当てはまるので

type MyAwaited<T> = T extends PromiseLike<infer A> 
  ? A extends Promise<any>
    ? MyAwaited<A>
    : A 
  : never;

としてもテストケースが通る。

If

type If<C, T, F> = any

type cases = [
  Expect<Equal<If<true, 'a', 'b'>, 'a'>>,
  Expect<Equal<If<false, 'a', 2>, 2>>,
  Expect<Equal<If<boolean, 'a', 2>, 'a' | 2>>,
]

というもの。
1つ目の入力値(Cにあたる部分)がtrueだったら2つ目の入力値(T)を、falseだったら3つ目の入力値(F)を返すもの。

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

としてあげればOK。
説明で「Cはtrueかfalseが期待されている」と記載されているので

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

としてあげると親切かも。

Concat

type Concat<T, U> = any

const tuple = [1] as const

type cases = [
  Expect<Equal<Concat<[], []>, []>>,
  Expect<Equal<Concat<[], [1]>, [1]>>,
  Expect<Equal<Concat<typeof tuple, typeof tuple>, [1, 1]>>,
  Expect<Equal<Concat<[1, 2], [3, 4]>, [1, 2, 3, 4]>>,
  Expect<Equal<Concat<['1', 2, '3'], [false, boolean, '4']>, ['1', 2, '3', false, boolean, '4']>>,
]

というもの。
JSのArray.concat()みたいな感じで型を結合させてね、といった感じ。

TとUがタプルの型が入ることを考えると

type Concat<T extends any[], U extends any[]> = any

としてextendsを書いてあげる。その後に通常の配列処理のようにスプレッド演算子を使って結合してあげる。

type Concat<T extends any[], U extends any[]> = [...T , ...U]

この場合にas constしてる定数tupleを使った Expect<Equal<Concat<typeof tuple, typeof tuple>, [1, 1]>> が通らなくなってしまうのでreadonlyのものを追加して

type Concat<T extends readonly any[], U extends readonly any[]> = [...T , ...U]

としたらOK。

Includes

type Includes<T extends readonly any[], U> = any

type cases = [
  Expect<Equal<Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Kars'>, true>>,
  Expect<Equal<Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'>, false>>,
  Expect<Equal<Includes<[1, 2, 3, 5, 6, 7], 7>, true>>,
  Expect<Equal<Includes<[1, 2, 3, 5, 6, 7], 4>, false>>,
  Expect<Equal<Includes<[1, 2, 3], 2>, true>>,
  Expect<Equal<Includes<[1, 2, 3], 1>, true>>,
  Expect<Equal<Includes<[{}], { a: 'A' }>, false>>,
  Expect<Equal<Includes<[boolean, 2, 3, 5, 6, 7], false>, false>>,
  Expect<Equal<Includes<[true, 2, 3, 5, 6, 7], boolean>, false>>,
  Expect<Equal<Includes<[false, 2, 3, 5, 6, 7], false>, true>>,
  Expect<Equal<Includes<[{ a: 'A' }], { readonly a: 'A' }>, false>>,
  Expect<Equal<Includes<[{ readonly a: 'A' }], { a: 'A' }>, false>>,
  Expect<Equal<Includes<[1], 1 | 2>, false>>,
  Expect<Equal<Includes<[1 | 2], 1>, false>>,
  Expect<Equal<Includes<[null], undefined>, false>>,
  Expect<Equal<Includes<[undefined], null>, false>>,

というもの。
JSのArray.include()みたいな感じで配列の中に指定した要素があったらtrue、なければfalseを返してね、といった感じ。

入ってるかみたいな判定にしたかったら

type Includes<T extends readonly any[], U> = U extends T ? true : false

みたいにしたいけど、Tがタプルの型でUがリテラル型なので常にfalseになってしまう。
なので1個1個要素を見ていくという方針で考えてみる。

Tの要素が1つ以上だったら1個目をUと比較→当てはまらなければ2個目を切り出してUと比較→...を最後まで繰り返していけばいいので

type Includes<T extends readonly any[], U> = T extends [infer H, ...any[]] ? [1個目をUと比較] : never

をしていく。かつその比較をループさせていく。

type Includes<T extends readonly any[], U> = T extends [infer H, ...infer R]
? Equal<H, U>
  ? true
  : Includes<R, U>
: false

みたいにしたいが、構文エラーになるので修正して

type Includes<T extends readonly any[], U> = T extends [infer H, ...infer R]
? Equal<H, U> extends true
  ? true
  : Includes<R, U>
: false

のようにEqual部分にextendsをつけてあげればOK。

Push

type Push<T, U> = any

type cases = [
  Expect<Equal<Push<[], 1>, [1]>>,
  Expect<Equal<Push<[1, 2], '3'>, [1, 2, '3']>>,
  Expect<Equal<Push<['1', 2, '3'], boolean>, ['1', 2, '3', boolean]>>,
]

とのこと。
JSのArray.push()のように配列(タプル)の末尾に要素を追加するイメージ。

単純にconcat同様くっつけるイメージで

type Push<T, U> = [...T, U]

にしたいけどTでスプレッド演算子を使用して良いんかいエラーが起こっているので

type Push<T extends any[], U> = [...T, U]

のようにTにextendsを付けてあげればOK。

Unshift

type Unshift<T, U> = any

type cases = [
  Expect<Equal<Unshift<[], 1>, [1]>>,
  Expect<Equal<Unshift<[1, 2], 0>, [0, 1, 2]>>,
  Expect<Equal<Unshift<['1', 2, '3'], boolean>, [boolean, '1', 2, '3']>>,
]

というもの。
JSのArray.unshift()のように配列(タプル)の先頭に要素を追加するイメージ。

これはpushと同じ処理で...TとUの順番を入れ替えてあげればいいので

type Unshift<T extends any[], U> = [U, ...T]

でOK。

Parameters

type MyParameters<T extends (...args: any[]) => any> = any

function foo(arg1: string, arg2: number): void {}
function bar(arg1: boolean, arg2: { a: 'A' }): void {}
function baz(): void {}

type cases = [
  Expect<Equal<MyParameters<typeof foo>, [string, number]>>,
  Expect<Equal<MyParameters<typeof bar>, [boolean, { a: 'A' }]>>,
  Expect<Equal<MyParameters<typeof baz>, []>>,
]

というもの。TypeScriptのParameters<>を使用せず、MyParametersでどうにかしてねということ。

関数に対してtypeofを使用すると (arg1: string, arg2: number) => void みたいな形になるので T extends (...args: any[]) => any のany[] 部分を抽出できればOKということでinferを使って定義すると

type MyParameters<T extends (...args: any[]) => any> =T extends (...args: infer P) => any ? P : never

としてあげればOK。

Discussion