Type Challenges 1〜
はじめに
Type Challengesとは?
このリポジトリ内にあるTypeScriptの型定義に関する問題(全179問)。
アルゴリズム系の問題はよくあるけど型定義の問題あるのとても良い👏🏻
日本語のドキュメント も用意してある。
例えば、Utility typeのPickを自前実装する問題とかがある。
手元で環境を整えずとも、ブラウザからアクセスできるTypescriptのPlaygroundでお手軽に実行できる。
モチベーション
ちょうど今、社内プロダクト開発時にGenerics、Conditional Type、inferを使い倒した複雑な型定義をしているが、未だに苦手意識があるので解いてみながらなくしていきたい。
「読んだらなんとなくわかる」から「ゼロから書ける」状態になりたい。
1. Hello World(Warm-up)
問題
解答
/* _____________ Your Code Here _____________ */
- type HelloWorld = any // expected to be a string
+ type HelloWorld = srting // expected to be a string
に書き換えるだけの問題。
/* _____________ Test Cases _____________ */
import type { Equal, Expect, NotAny } from '@type-challenges/utils'
type cases = [
Expect<NotAny<HelloWorld>>,
Expect<Equal<HelloWorld, string>>,
]
独自の型を定義して、それを配列に入れたものをテストケースとしている。
なるほどなぁ。
Equal
の中身
遅延評価なんてTtypeScript書きながら考えたことないな。。
改めて勉強
どちらもuhyoさんの記事。
読んで一発で理解、は到底無理なので何度も参照したい。
2. Pick(Easy)
問題
解答
type MyPick<T, K extends keyof T> = {
[key in K]: T[key]
}
メモ
Equalの挙動を調べる際に復習がてらここらへんを眺めていたので extends keyof はすっと出てきた。
右辺がわからなかった。
Goodが多かったここからコードを拝借。
type name = 'firstname' | 'lastname'
type TName = {
[key in name]: string
}
// TName = { firstname: string, lastname: string }
extends keyof
と in keyof
の違い
上のIssueで取り上げられていたので改めて調べてみる。
extends keyof
- ある型のプロパティのキーを使って型制約を設けるために使われる。
- 具体的には、型引数が特定のオブジェクト型のプロパティ名であることを保証する。
type Person = {
name: string;
age: number;
};
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const person: Person = { name: 'Alice', age: 30 };
const name = getProperty(person, 'name'); // ✅ Success
const age = getProperty(person, 'age'); // ✅ Success
// const invalid = getProperty(person, 'height'); // 🚫 Error
in keyof
- オブジェクト型のプロパティ名を列挙して新しい型を定義するために使われる。
- 具体的には、マッピング型(Mapped Type)を作成する際に使用される。
マッピング型というのは { [P in K]: T }
という形。
// OriginalTypeのすべてのキーKに対してTransformationを適用し、新しい型MappedTypeを定義する
type MappedType = {
[K in keyof OriginalType]: Transformation;
};
in
はstring, number, symbol literalの和で型付けしたいインデックスシグネチャを定義する時に使う。
keyofと組み合わせることで、元の型のすべてのプロパティを再マッピングした、いわゆるマップされた型を作成することができる。
インデックスシグネチャってなんだっけ。
インデックスシグネチャの定義は
{[key:T]:U}・・・このオブジェクトについては型Tの全てのキー(プロパティ名)は、型Uの値を持たなければならないことを意味します。
型Tは'string' または 'number' のいずれかである必要があります。
今回の解答の[key in K]:
の部分はまさにインデックスシグネチャなのか。
K extends keyof T
ということは K
は T
のキーなのでT
に何が来てもK
はstring | number
という推論が成り立っているということなんだろうか。
type Person = {
name: string;
age: number;
};
// すべてのプロパティをオプショナルにする
type PartialPerson = {
[K in keyof Person]?: Person[K];
};
const partialPerson: PartialPerson = { name: 'Alice' }; // 部分的にプロパティが設定されている
// すべてのプロパティの型を変更する
type ReadOnlyPerson = {
[K in keyof Person]: Readonly<Person[K]>;
};
const readOnlyPerson: ReadOnlyPerson = { name: 'Alice', age: 30 }; // ✅ Success
// readOnlyPerson.name = 'Bob'; // 🚫 Error
頭こんがらがる。
3. Readonly(Easy)
問題
解答
Readonly<T>
が使えない制限ならreadonly
を全てのプロパティに適用させるコードを書けば良いのですぐわかった。
とはいえ、これがすぐわかったの1つ前の問題を解いていたからだな。
インデックスシグネチャでマッピング型を作成(学習した言葉をすぐ使う)。
type MyReadonly<T> = {
readonly [key in keyof T] : T[key]
}
メモ
そういやマッピング型について調べてる時に +readonly
とか出てきたんだけどこういうのも使っていくのだろうか。
4. Tuple to Object(Easy)
問題
解答
[key in T]: key
まで書いた後に T
にエラーが出ていて、インデックスで要素にアクセスすればいいから...とT[number]
をしてみたら通った。
// type TupleToObject<T extends readonly PropertyKey[]> ={ // 👍 Better
type TupleToObject<T extends readonly any[]> ={
[key in T[number]]: key
}
ref: インデックスアクセス型 (indexed access types) | TypeScript入門『サバイバルTypeScript』
メモ
Tuple型
- 長さが固定されている
- 型が決定している
- 各要素の型があらかじめ決められており、その順序も固定されている
- 例えば、
[number, string]
というタプルは、最初の要素が number、次の要素が string である必要がある
PropertyKey
// type PropertyKey = string | number | symbol;
type TupleToObject<T extends readonly PropertyKey[]> ={
[key in T[number]]: key
}
PropertyKey
使ってる方がいいな。
Symbol型って存在はしっているものの使い道がわかってない。
うっすらわかった気になった。
5. First of Array(Easy)
問題
解答
type First<T extends any[]> = T extends [] ? never : T[0]
メモ
最初はなんとなく以下のように書いたらneverだけでエラーが出た。
三項演算子で回避して解決。
type First<T extends any[]> = T[0]
type cases = [
Expect<Equal<First<[3, 2, 1]>, 3>>,
Expect<Equal<First<[() => 123, { a: string }]>, () => 123>>,
Expect<Equal<First<[]>, never>>, // 🚫 Error
Expect<Equal<First<[undefined]>, undefined>>,
]
6. Length of Tuple(Easy)
問題
解答
type Length<T extends readonly string[]> = T['length']
メモ
最初はこんなかんじで書いた。
type Length<T extends any[]> = T.length
Cannot access 'T.length' because 'T' is a type, but not a namespace. Did you mean to retrieve the type of the property 'length' in 'T' with 'T["length"]'?
というエラーが.length
で出ているので言われた通りT["length"]
にしてみる。
Type 'readonly ["tesla", "model 3", "model X", "model Y"]' does not satisfy the constraint 'string[]'.
The type 'readonly ["tesla", "model 3", "model X", "model Y"]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.
次はtypeof
の箇所でこのようなエラーが出た。
ミュータブルな型になっているのでreadonly
を付けて完成。
7. Exclude(Easy)
問題
解答
type MyExclude<T, U> = T extends U ? never : T
メモ
ユニオン型
|
で繋いでるからといって、ユニオン型ではないのか?
type Example = string | number
みたいなやつだよな。
TypeScriptのユニオン型(union type)は「いずれかの型」を表現するものです。
ref: ユニオン型 (union type) | TypeScript入門『サバイバルTypeScript』
いや、以下のようなコードもあるからこれもある意味ユニオン型とも言えるのか。
type ErrorCode = 400 | 401 | 402
type Example = 'a' | 'b' | 'c'
というふうにも書けるわけだし。
<T, U>
の方
type MyExclude<T, U> = any
って最初から書いてあるけどユニオン型をT
で受け取ったとして...?
別にU
に入ってくるものをT
で保証する必要とか特に無いから<T, U extends T>
的なのも特に書く必要はないはず。
any
の方
多分こっちをどうにかせねばな感じがする。
U
の中でT
をextendsしてるものを返すことになるので、U extends T ? U : never
としてみたけど違いそう。
ん?なんか脳みそでIncludeを考えてた。真逆だ。
最終的に返すのはT
の値なので三項演算子は never
と T
にして以下解答。
type MyExclude<T, U> = T extends U ? never : T
8. Awaited(Easy)
問題
解答
わからなくて答え見た。
type MyAwaited<T extends PromiseLike<any>> = T extends PromiseLike<infer U>
? U extends PromiseLike<any>
? MyAwaited<U>
: U
: never;
type MyAwaited<T> = T extends PromiseLike<infer U> ? MyAwaited<U> : T
誤答
type MyAwaited<T> = T extends Promise<infer U> ? U : never;
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>>, // 🚫
]
最初こんな感じにしてみたが、テストが通らないものがあった。
メモ
infer調べてたらこの記事に遭遇した色々勉強する。
型の勉強をするとなんか聞いたことあるけどいまいちピンときてない言葉で、とてもよくわからない言葉の説明をされてしまうのどうにかせねば。
アサーション
- 型アサーション
as hoge
- constアサーション
-
as const
。readonlyが付与される
-
assertionは「主張・言明」の意。
データ型
データ型は2種類ある
-
プリミティブ型
- イミュータブル(値を直接変更できない)
- プロパティを持たない
- 7種類
- boolean型(論理型): trueまたはfalseの真偽値。
- number型(数値型): 0や0.1のような数値。
- string型(文字列型): "Hello World"のような文字列。
- undefined型: 値が未定義であることを表す型。
- null型: 値がないことを表す型。
- symbol型(シンボル型): 一意で不変の値。
- bigint型(長整数型): 9007199254740992nのようなnumber型では扱えない大きな整数型。
- オブジェクト
- ミュータブル(変更可能)
- プロパティを持つ
Q:string型は"name".length
の形式でプロパティを持つのでは?
A:JavaScriptにはプリミティブ型をオブジェクトに自動変換するオートボクシングという機能があるため、プリミティブ型でもまるでオブジェクトのように扱える
リテラル型
- プリミティブ型の特定の値だけを代入可能にする型のこと
- 一般的にリテラル型はマジックナンバーやステートの表現に用いられます。その際、ユニオン型と組み合わせることが多い
- リテラル型として表現できるプリミティブ型は3種類
- boolean型のtrueとfalse
- number型の値
- string型の文字列
// 例1
let x: 1;
x = 1; // プリミティブ型→リテラル型になる
x = 100;
// 例2
const isTrue: true = true;
const num: 123 = 123;
const str: "foo" = "foo";
// 例3
let num: 1 | 2 | 3 = 1; // ユニオン型との組み合わせ
Widening Literal Types、NonWidening Literal Types
リテラル型における推論方法の違い。
リテラル型がより一般的な型に「拡張」されるかどうかを制御する。
- Widening Literal Types
- Widening Literal Types は、リテラル型がより広い型(例えば string や number)に自動的に変換(拡張)されることを指します。これは特に変数宣言時に発生します。
-
// ここで x の型は string 型として推論される。よって後から"hello"以外の文字列を代入できる let x = "hello";
- NonWidening Literal Types
- リテラル型がそのまま特定のリテラル型として保持され、より広い型に自動的に変換されないことを指す。
- 特に const 宣言や明示的な型注釈を用いる場合に発生する
-
// ここで y の型は "hello" というリテラル型として推論されるため"hello"以外代入できない const y = "hello";
- ※ 調査不足だけどただ単に
const
で定義しただけではどっちのリテラル型でもあり、constアサーションを使った時にNonWidening Literal Typesになるとこの記事では書いてあった
infer
- 直訳すると「推論」の意。
- Conditional Type(
type IsNumber<T> = T extends number ? true : false;
みたいな)で使われるもの- 使える場所はextendsの条件部分に限定されている
// T が id というプロパティを持っている場合は、その id の型を返す。
// もし、id というプロパティがなければ never を返す。
type Id<T> = T extends { id: infer U } ? U : never;
ChatGPTにはまさにPromiseのことを教えられた。
ReturnType
inferの例としてUtility typeのReturnTypeがよく取り上げられていたので勉強しておく。
要は関数を入れたらその返り値の型を取得してくれるやーつ。
function greet(): string {
return "Hello, TypeScript!";
}
type GreetingType = ReturnType<typeof greet>; // = string
const user = {
id: 1,
name: "Taro",
getInfo() {
return {
id: this.id,
name: this.name
};
}
};
type UserInfoType = ReturnType<typeof user.getInfo>; // { id: number; name: string; }
async function fetchData(): Promise<number> {
return 10;
}
// これだとPromise<number>が返ってくる
// type FetchedData = ReturnType<typeof fetchData>;
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type FetchedData = UnwrapPromise<ReturnType<typeof fetchData>>;
このサイトは実例が色々載っていてわかりやすかった。高度なマッピング型の例とか今後Type Challengeでも出てきそう。
この人は「TypeScript一人カレンダー」をやっているので他の記事も見てみる。
PromiseLike
恥ずかしながら解答見て始めて存在を知った。
9. If(Easy)
問題
解答
type If<C extends boolean, T, F> = C extends true ? T : F
メモ
過去一簡単だった。
type If<C extends boolean, T, F> = C extends true ? T : F
import type { Equal, Expect } from '@type-challenges/utils'
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つずつテスト通してくか、の精神でなんとなく書いたらテストが通ってしまった。
なんでこの場合3つ目のテストがOKになるんだと思ったけど、boolean
型の場合C
はtrue
false
どちらにもなりうるから両方のケースがカバーされて'a' | 2
になるということか。
10. Concat(Easy)
問題
解答
type Concat<T extends readonly any[], U extends readonly any []> = [...T, ...U]
経緯
まずはシンプルに書いてみる。
type Concat<T extends any[], U extends any []> = [...T, ...U]
Type 'readonly [1]' does not satisfy the constraint 'any[]'.
The type 'readonly [1]' is 'readonly' and cannot be assigned to the mutable type 'any[]'.
readonly [1]
型は any[]
ではない、と。
tupleの型ってany型に入らないのか?
調べてみるとReadonlyArray<T>
readonly T[]
という書き方があるじゃないか(2つに違いはない)。
これを付け足して解決。
メモ
別解を見る
別解ないかとりあえず一番評価されているのを見てみる。
type Tuple = readonly unknown[];
type Concat<T extends Tuple, U extends Tuple> = [...T, ...U];
any
と unknown
の違いを明確に言語化できるように改めて勉強しよう。
any
と unknown
-
any
- 説明
- 型安全性を無視し、あらゆる操作が許容される
- ユースケース
- 型チェックを回避したいときに使用
- 乱用すると型安全性が失われる
- 使用例
-
let value: any; value = 42; value = "hello"; value = { key: "value" }; // 型チェックが無効 value.toUpperCase(); // エラーなし value.someMethod(); // エラーなし
-
- 説明
-
unknown
- 説明
- 型安全性を保ち、操作の前に型チェックが必要
- つまり、値の型が確定するまで操作を行うことができない
- 任意の値が割り当て可能だが、
any
よりも型安全性を維持する
- 型安全性を保ち、操作の前に型チェックが必要
- ユースケース
- 型安全性を維持しつつ、任意の値を扱う必要がある場合に使われる
- 特に、外部からの入力や動的なデータの処理に適している
- 使用例
-
let value: unknown; value = 42; value = "hello"; value = { key: "value" }; // 型チェックが有効 if (typeof value === "string") { value.toUpperCase(); // OK } else { // value.toUpperCase(); // エラー: 型 'unknown' にプロパティ 'toUpperCase' は存在しません } if (typeof value === "object" && value !== null) { // 型アサーションを使う console.log((value as { key: string }).key); // OK }
-
- 説明
11. Includes(Easy)
問題
解答
パターン1
うわ〜。T extends [infer First, ...infer Last]
とかまんま見たことあるな〜。
あと、importしてるEqual
を使うとか1mmも考えなかった。
何日か後にもう一度やろう。
type Includes<T extends readonly any[], U> = T extends [infer First, ...infer Last] ?
Equal<U, First> extends true ?
true :
Includes<Last, U> :
false
-
T extends [infer First, ...infer Last]
-
T
が少なくとも1つの要素を持つ配列に分解できるかをチェックする -
First
は配列の最初の要素に割り当てられ、Rest
は残りの要素に割り当てらる
-
-
Equal<U, First> extends true
は、U
とFirst
が同じかどうかをチェック -
Includes<Rest, U>
- 異なる場合は、再帰的に残りの要素の中でUを探す
- 再帰していき
T
が空配列になると再帰が終了しfalseを返す
パターン2
type MyEqual<X, Y> = (<T>() => T extends X ? 1 : 2 ) extends (<T>() => T extends Y ? 1 : 2) ? true : false
type Includes<T extends readonly any[], U> = true extends {
[I in keyof T]: MyEqual<T[I], U>
}[number] ? true : false
-
{ [I in keyof T]: MyEqual<T[I], U> }
- 配列
T
の各要素とU
を比較するマッピング型 -
keyof T
は、配列T
のすべてのインデックスキーを取得する -
T[I]
は、T
の各インデックスI
に対応する要素の型を取得する -
MyEqual<T[I], U>
は、各要素T[I]
がU
と等しいかどうかをチェックする
- 配列
-
{ [I in keyof T]: MyEqual<T[I], U> }[number]
- マッピング型のすべての値をユニオン型として取得する
- 例えば、
T
が['a', 'b', 'c']
であり、U
が'a'
である場合、これはMyEqual<'a', 'a'> | MyEqual<'b', 'a'> | MyEqual<'c', 'a'>
というユニオン型になる
-
true extends { [I in keyof T]: MyEqual<T[I], U> }[number]
- ユニオン型に
true
が含まれているかどうかをチェックする - もし含まれていれば、全体として
true
が返され、含まれていなければfalse
が返される
- ユニオン型に
メモ
正解にたどり着けず
// type Includes<T extends readonly any[], U> = any // 初期状態
type Includes<T extends readonly any[], U> = U extends T ? true : false
まずは一旦こんな感じに。
結果がfalse
のものは通ったがtrue
のものはすべて通らず。
U extends T
が違うんだろうな〜。
infer
をいい感じに使いたいんだけどわからん。
in
とかもどうやら使えないし...。
で、ギブアップ。
12. Push(Easy)
問題
解答
type Push<T extends PropertyKey[], U> = [...T, U]
13. Unshift(Easy)
問題
解答
type Unshift<T extends unknown[], U> = [U, ...T]
14. Parameters(Easy)
問題
解答
type MyParameters<T extends (...args: any[]) => any> =
T extends (...any: infer S) => any ? S : any
T
が (...any: infer S) => any
の形式にマッチする場合、S
を抽出。
マッチしない場合は any
を返す。
15. Get Return Type(Medium)
やっとMediumだ〜〜〜〜〜。
問題
解答
type MyReturnType<T extends Function> =
T extends (...args: any) => infer R
? R
: never
16. Omit(Medium)
問題
解答
type MyOmit<T, K extends keyof T> =
{ [P in keyof T as P extends K ? never : P]: T[P] }
-
P
はT
のキーの1つ(e.g."description"
) -
P
がK
に含まれている場合(P extends K
)never
を返す = そのキーを除外する- 含まれていない場合
P
を保持
- 含まれていない場合
Type Challengeの問題ではないけれど偶然発見したのでメモ。
これにしたいというケース今まで無かったけど、実は厳密には「空ではない配列型」の方がいい実装ありそうな気はする。
17. Readonly 2(Middle)
問題
解答
&
の後からわからんかった...。Middleの時点で結構むずいな。
as P
とか1個前でも使われてるし今一度復習してから次に行かないと身につかなさそう...。
type MyReadonly2<T, K extends keyof T = keyof T> = {
readonly[P in K] : T[P]
} & {
[P in keyof T as P extends K ? never: P] : T[P]
}