type-challengesに挑戦【初級編】

まずは初級編からやってみる

Hello World
HelloWorld型を変えて、型エラーを直す
type HelloWorld = any
type test = Expect<Equal<HelloWorld, string>> // エラー
回答
type HelloWorld = string
解説
なし
メモ
エラーチェックで使用している型について
Expect型
T
に渡される型がtrue
であることをextends true
で制限している
つまりT
がtrue
以外だとエラー
Equal型
T extends X
とT extends Y
の結果を比較している
つまりX
とY
が同じ型でなければfalse
になる

Pick
組み込みの型ユーティリティ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,
}
Todo
型から'title' | 'completed'
のプロパティだけを抜き出し、TodoPreview
型として使うということ
Pick
を使用すると以下
type TodoPreview = Pick<Todo, 'title' | 'completed'>
回答
type MyPick<T, K extends keyof T> = { [key in K]: T[K] }
解説
-
K extends keyof T
で、K
がT
のキー(プロパティ名)であることを制限
→Todo
のキーは'title' | 'description' | 'completed'
-
{ [key in K]: T[key] }
は、Mapped Type
を使いK
の各キーを順に取り出して、対応するT
のプロパティ型を新しいオブジェクト型のプロパティ型として設定
メモ
keyof
keyofはオブジェクトの型からプロパティ名を型として返す型演算子です。
Mapped Types
Mapped Typesは主にユニオン型と組み合わせて使います。
インデックス型と同じようにキーの制約として使用することができます。

Readonly
組み込みの型ユーティリティ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
回答
type MyReadonly<T> = { readonly [K in keyof T]: T[K] }
解説
-
keyof T
は、T
のプロパティ名すべてのユニオン型("title" | "description"
)を表す -
[K in keyof T]
はMapped Type
で、T
のすべてのプロパティ名K
を順番に取り出す - ⭐️
readonly
を付けて各プロパティを読み取り専用に -
T[K]
はプロパティK
の型を意味し、元の型のまま値の型を維持
メモ
readonly

Tuple to Object
タプルを受け取り、その各値のkey/valueを持つオブジェクトの型に変換する型を実装する
const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const
type result = TupleToObject<typeof tuple> // expected { 'tesla': 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}
回答
type TupleToObject<T extends readonly (string | number | symbol)[]> = {
[P in T[number]]: P
}
解説
- ⭐️
T[number]
で配列やタプルの全要素の型をユニオン型として取り出す
→ 展開すると'tesla', 'model 3', 'model X', 'model Y'
-
[P in T[number]]
はMapped Type
で、ユニオン型の各要素P
を順にキーとして取り出す -
{ [P in T[number]]: P }
でキーと同じ値を持つオブジェクト型を作成
メモ
タプルとは?
- 要素の数と型が決まっている配列
- 例えば
[string, number]
は「最初が文字列、次が数値」の2つの要素を持つ - 配列との違いは、配列は同じ型の要素が何個でも並ぶのに対し、タプルは型も個数も固定されている点
https://typescriptbook.jp/reference/values-types-variables/tuple

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
回答
type First<T extends any[]> = T extends [] ? never : T[0]
解説
- ⭐️
T extends [] ?
で配列T
が空かどうかを判定し、空ならtrue
としてnever
を返す -
T[0]
は配列の先頭の要素の型を指すので、空でない場合T[0]
がそのまま返る - 結果として「配列が空なら
never
、そうでなければ最初の要素の型」を返す型になる
これだけでもいいのでは?と思ったが、空配列の場合undefined
になるのでnever
を返した方が安全
type First<T extends any[]> = T[0];
メモ
never型
never型は「値を持たない」を意味するTypeScriptの特別な型です。

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
回答
type Length<T extends readonly any[]> = T['length']
解説
-
T extends readonly any[]
で、T
はタプルまたは読み取り専用の配列であることを制限
→readonly
がないとas const
で定義されたリテラルタプルが代入できない -
T['length']
で、配列やタプルのlength
プロパティを使って要素数を取得
メモ
as const
変数宣言のときに、末尾にas constをつけるとその値をreadonlyにした上で、リテラル型にしてくれます。
→ 型はreadonly ['a', 'b']
になり、各要素の型も'a' | 'b'
ではなく"a" や "b"
などのリテラル型になる

Exclude
組み込みの型ユーティリティExclude <T, U>
を使用せず、U
に割り当て可能な型をT
から除外する型を実装する
type Result = MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'
Exclude
を使用すると以下
type Result = Exclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'
回答
type MyExclude<T, U> = T extends U ? never : T
解説
-
T extends U ? never : T
はConditional Typeを使用して、T
がU
に割り当て可能(T extends U
)かどうかをチェック
→ 割り当て可能ならnever
、不可能ならT
を残す =T
からU
を取り除く

Awaited
Promise<ExampleType>
という型がある場合、どのようにしてExampleType
を取得すればよいか
type ExampleType = Promise<string>
type Result = MyAwaited<ExampleType> // string
Awaitedを使うと以下
type Result = Awaited<ExampleType> // string
回答
type MyAwaited<T extends PromiseLike<any>> = T extends PromiseLike<infer U>
? U extends PromiseLike<any>
? MyAwaited<U>
: U
: never;
解説
- ⭐️
T extends PromiseLike<any>
で、T
はPromise
またはthen()
を持つことを制限
→Promise<T>
やPromise<Promise<T>>
に対応可能に - ⭐️
T extends PromiseLike<infer U>
で、PromiseLike
の中身の型をU
として取り出す
(例:Promise<string>
→U = string
) -
U extends PromiseLike<any>
で、U
がまたPromise
であれば再びMyAwaited<U>
を実行(再帰処理)し、そうでない場合U
を返す -
T
がPromiseLike
でない場合はnever
を返す
🗣️ むずー
メモ
PromiseLike
Promiseのcatchメソッドがない版
infer
型の中から特定の型を“推論”(infer)して取り出すことができる
使える場所はconditional typesのextends
の条件部分に限定される

If
C
がtruthy
である場合の戻り値の型T
、C
がfalsy
である場合の戻り値の型F
を受け取るIf
を実装する
type A = If<true, 'a', 'b'>; // expected to be 'a'
type B = If<false, 'a', 'b'>; // expected to be 'b'
回答
type If<C extends boolean, T, F> = C extends true ? T : F
解説
-
C extends boolean
でC
をboolean
に制限 -
C extends true ? T : F
でConditional typesを使用しC
がtrue
なら型T
を返し、そうでなければ型F
を返す

Concat
JavaScriptのArray.concat
関数を型システムに実装する
type Result = Concat<[1], [2]>; // expected to be [1, 2]
実際に配列を結合するのではなく、「結合後の配列の形」を型として表現するということ
回答
type Tuple = readonly unknown[];
type Concat<T extends Tuple, U extends Tuple> = [...T, ...U];
解説
-
Length of Tupleでやったように、
readonly
を付けることでas const
な配列にも対応可能に -
T extends Tuple, U extends Tuple
で、T
とU
は両方とも配列(またはタプル)型に制限 - ⭐️
[...T, ...U]
で、スプレッド構文を使いT
とU
の要素を結合した新しいタプル型を作成

Includes
JavaScriptのArray.include
関数を型システムに実装する
type isPillarMen = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> // expected to be `false`
回答
type Includes<T extends readonly any[], U> = T extends [infer L, ...infer R]
? [U, L] extends [L, U]
? true
: Includes<R, U>
: false
解説
-
T extends [infer L, ...infer R]
で配列T
を分割し、先頭要素をL
、残りの要素をR
に取り出す(再帰処理の土台) -
[U, L] extends [L, U]
で、型U
がL
と等しいかどうかを比較する
タプルでまとめて両方向比較することで完全一致を確認できる(×U extends L
) - ⭐️続いて
? true : Includes<R, U>
で、U
がL
に一致すればtrue
、一致しなければ残りの要素R
に対して再帰的にIncludes<R, U>
を呼ぶ -
: false
(ベースケース)で、配列T
が空になったら(つまり最後まで一致しなかった)らfalse
を返す
🗣️ infer使いこなせる気がしない
🗣️ 再帰なんて思いつかない...

Push
Array.push
のジェネリックバージョンを実装する
type Result = Push<[1, 2], '3'> // [1, 2, '3']
回答
type Push<T extends unknown[], U> = [...T, U]
解説
-
T extends unknown[]
でジェネリック型T
は配列であることを指定 -
U
は追加する要素の型 - ⭐️
[...T, U]
で、スプレッド構文を使ってT
のすべての要素の後ろにU
を追加した新しいタプルを生成
メモ
ジェネリック型
どんな型(T)でも受け取れるように作られた型

Unshift
Array.unshift
の型バージョンを実装する
type Result = Unshift<[1, 2], 0> // [0, 1, 2]
回答
type Unshift<T extends unknown[], U> = [U, ...T]
解説
-
T extends unknown[]
でジェネリック型Tは配列であることを指定 -
U
は配列の先頭に追加したい要素の型 - ⭐️
[U, ...T]
で、スプレッド構文を使ってU
を先頭に置き、その後に配列型T
を追加した新しいタプルを生成

Parameters
組み込みの型ユーティリティParameters<T>
を使用せず、T
からタプル型を構築する型を実装する
const foo = (arg1: string, arg2: number): void => {}
type FunctionParamsType = MyParameters<typeof foo> // [arg1: string, arg2: number]
Parameters
を使用すると以下
type Params = Parameters<typeof foo> // [arg1: string, arg2: number]
回答
type MyParameters<T extends (...args: any[]) => any> =
T extends (...any: infer S) => any ? S : any
解説
-
T extends (...args: any[]) => any
で、T
は「引数が任意の配列、戻り値は任意の関数型」であることを制限 - ⭐️
T extends (...any: infer S) => any
はT
の引数の型をS
として推論(infer) -
? S : any
は条件に合致すれば引数の型のタプルS
を返し、そうでなければany
を返す
→ 関数の引数が複数ある場合、引数の型は順序付きの複数型の集合として「タプル型」で表現される
→ そのため引数の型をまとめて取り出すとタプル型として返される

初級編終了。。これが初級編...?😇