【お試し・初級】TypeScriptのtype-challengesをやってみた
🏁 概要
TypeScriptの型の概念を勉強するために型パズル、type-challengeをやったときのメモ。
type-challengesとは型定義についてanyとなっているところを書き直してテストケースが通るようにしようね、という問題集。
テストケースはExpect<Equal<A, B>>となっておりAとBが一致するように書いてあげる。
やった時時点の問題一覧はこんな感じ。

中級以降は問題が格段に多くなるため、本記事では「お試し」と「初級」にある問題について記載。
🪢 知っておくべき知識
各問題を特にあたって知っておかないと🤯ってなりそうな知識を記載。
問題の解説を見ても分からなかったらここに戻ってきてみようの区画。
型名
ユーティリティ型
その他
- as const
- Conditional Types
- extends
- infer
- keyof
- Mapped Types
- Promise<T>
- PromiseLike
- readonly
- typeof
- タプル
- 分割代入(残余プロパティ)
🧪 問題解説 - お試し
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