Type<Challenge[]>~easy?~
2022-06-05
Photo Credit: Vardan Papikyan
https://github.com/type-challenges/type-challenges
type-challengesに真面目に取り組もう取り組もうと思いつつ、放置してしまっていました。
ただ、3ヶ月の目標に「type-challengesのeasyは自力で解けるようになる。」という目標を入れてしまっていたので、真面目に取り組んでいきます。
2022年6月現在、easyは全部で13問用意されているので、それらを順番に解いて、解説していきます。
00004-Pick
TypeScript組み込み機能の Pick<T,K>
を実装していきます。
最終的には、以下のようなコードになります。
type MyPick<T, K extends keyof T> = { [key in K] : T[key] };
完全に初めてこれを解いたときには、「本当にeasy1問目。。?」となりました。
一つづつ分解します。
keyof (keyof演算子)
The keyof
type operator には、「オブジェクトをとり、keyとして含まれるstringもしくはnumber型のユニオン型を生成します」と書かれています。
説明そのままで、以下のようにkeyからユニオン型を生成することができます。これによって、わざわざオブジェクトとは別にユニオン型を定義する必要がなくなるので、オブジェクトに変更を加えた際にユニオン型を手動で変更する必要がなくなり、保守性が向上します。
type Person = {name: string, age: number, 100:number}
type PersonKeys = keyof Person // "name"|"age"|100
これを今回の回答に適用すると、例えば T
が Person
型だった場合、以下状態と解釈できます。
type MyPick<T, K extends "name"|"age"|100> = { [key in K] : T[key] };
extends (Genericsにおける型引数の制約)
Genericsで extends
を活用すると、 A extends B
と記載した場合に、型 A
を型 B
及び型 B
から継承して作成された型に制限して活用することができるようになります。
また、interfaceに対しても extends
を活用することが可能で、これはclassにおける implements
を行ったのと同じような挙動をします。
いずれにしても、 A
の型を絞り込むために活用します。ドキュメントの例がわかりやすいのでそのまま転記しておきます。
// Error
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length);
// Property 'length' does not exist on type 'T'.
return arg;
}
// No error
interface Lengthwise {
length: number;
}
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}
//https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-constraints
これを元に、今回の回答にコメントを足します。
// KはオブジェクトTが持つキー値である"name", "age", 100のいづれかである。
type MyPick<T, K extends "name"|"age"|100> = { [key in K] : T[key] };
in (Mapped type)
Mapped typeはユニオン型と組み合わせて活用されます。オブジェクトのキーをユニオン型で定義した値に制限することが可能です。
type MyFamily = "Taro" | "Jiro" | "Saburo";
type MyFamilyAge = { [k in MyFamily]: number };
const family: MyFamilyAge = { Taro: 20, Jiro: 18, Saburo: 15 };
//Error
//Property 'Saburo' is missing in type '{ Taro: number; Jiro: number; }' but required in type 'MyFamilyAge'.ts(2741)
const familyError: MyFamilyAge = { Taro: 20, Jiro: 18};
この機能を型定義の中で活用することで、仮想的に以下のように分解されます。(このコードはこのままではエラーになります。)
つまり、ユニオン型の要素(=Tが持つキーに含まれる値)をそれぞれキーとして持ち、かつそれぞれに対応する型を持つinterface型になります。
// KはオブジェクトTが持つキー値である"name", "age", 100のいづれかである。
type MyPick<T, K extends "name"|"age"|100> = {
name : T["name"],
age: T["age"],
100: T[100]
};
まとめ
type MyPick<T, K extends keyof T> = { [key in K] : T[key] };
const obj1 = { name: "Taro", age: 20, address: "tokyo" };
const obj2: MyPick<typeof obj1, "name"> = { name: "Jiro" }; //OK
const obj3: MyPick<typeof obj1, "name"> = { name: "Jiro", age: 18 };//Error
const obj4: MyPick<typeof obj1, "name" | "age"> = { name: "Jiro" };//Error
const obj5: MyPick<typeof obj1, "school"> = { shool: "Hoge High School" };//Error
obj3: age
キーはGenericsの第二引数に含まれないのでエラーになります。
obj4: age
キーがGenericsの第二引数のユニオン型に含まれているにも関わらず、右辺で age
キーをもつプロパティが定義されていないのでエラーになります。
obj5: school
キーがGenericsの第二引数に定義されていますが、 school
は typeof obj1
に含まれないキーなので、extendsで弾かれてエラーになります。
00007-readonly
TypeScript組み込みの Readonly<T>
を実装していきます。
最終的なコードは以下です。
type MyReadonly<T> = {readonly [key in keyof T]: T[key]};
readonly (readonly property)
オブジェクトのプロパティに対して、 readonly
修飾子をつけることによって、対象を読み取り専用にすることができます。
const myObj = {readonly name:"Taro", age:20}
console.log(myObj.name) //OK
myObj.name = "Jiro" //Error
その他の要素は00004-Pickと同じなので割愛します。
00011-tuple-to-object
tuple型で定義した変数を、そのままオブジェクトに変換する場合の型です。
type TupleToObject<T extends readonly (keyof any)[]> = { [t in T[number]]: t };
Playground_00011_tuple_to_object
playgroundに用意されている以下の条件も通すためには、上の実装がいいかなーと思っているのですが、厳密にやろうとするともう少しいい解答もないかなー。と思っています。
// @ts-expect-error
type error = TupleToObject<[[1, 2], {}]>
keyof any
keyof any
は any
のキー値、つまり string | number | symbol
と同じです。
最初これを見たときには、「 boolean
は入 らないのか?」と思ったのですが、 boolean
は含まれません。
type Bool = {boolean: number} //OK
const ok = {true:1, false:2}//OK
const err = {true:1, false:2, true:3}//Error
上のコードのように複数回同じ値をキーに取った場合にはエラーになります。しかしこの挙動は string
や number
でも同じだろ。と思っているのですが、試しに解答を以下のコードに変えてみる とエラーになります。
//Error
type TupleToObject<T extends readonly (string|number|symbol|boolean)[]> = { [t in T[number]]: t };
//error message
//Type 'T[number]' is not assignable to type 'string | number | symbol'.
// Type 'string | number | boolean | symbol' is not assignable to type 'string | number | symbol'.
// Type 'boolean' is not assignable to type 'string | number | symbol'.(2322)
これはMapped types自体の制約で、 {[K in U]: T}
としたときに、制約型である U
は string, number, symbolの部分型である必要があります。
また、この例のようにTがKに依存しないケースは Record
型として定義されており、以下のように活用できます。keysに代入できるのは、string, number, symbol及びそれらのリテラル型で、オブジェクトのプロパティであるTには任意の型が代入可能です。
// Record<Keys,T>
type Area = Record<"North"|"Middle",string>;
const area:Area = {
North: "nothing",
Middle: "stores"
}
00014-first-of-array
配列の最初の要素を型として返す型です。
type First<T extends any[]> = T extends [infer A, ...infer R] ? A : never
Playground_00014_first_of_array
infer(Type inference in conditional types)
inferを理解するためには、conditional typesを理解しておく必要があります。
conditional types
conditional typesは見た目の通り、三項演算子を型定義において利用するものです。
T extends U ? A : B
T extends U
がtrueだった場合に、型がAに決まり、falseだった場合にBに決まります。
三項演算子とほぼ同じなので直感的にも理解しやすいかと思います。
infer
本題のinferです。inferは日本語にすると「推論」になりますが、その名の通り推論した型を活用するための技術です。conditional typesと合わせて活用します。
説明だけだと分かりにくいのでまず公式ドキュメントに出ている例を見て、その後説明していきます。
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
Genericsでは、引数で渡した型を使い回す形になりますが(今回のTypeのように)、inferでは動的に型の値を変形させて活用することが可能です。今回の場合、 Type
がなんらかの要素を持った配列だった場合、その要素の型を返します。
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
const myArr1 = [1,2,3]
const myArr2 = [1,2,"3"]
type MyArr1 = Flatten<typeof myArr1> // number
type MyArr2 = Flatten<typeof myArr2> // number | string
このように、inferを活用することでインデックスによるアクセスなどで要素の型に直接アクセスすることなく、推論によって型の値を得ることができるようになります。
ここまで見るとわかるように、最初の回答はinferを使わずに以下のように書き換えることも可能です。
type First<T extends any[]> = T extends [T[0], ...T[number]] ? T[0] : never
また、ややこしいことはせずこれでもOKです。
type First<T extends any[]> = T extends [] ? never : T[0]
00018-tuple-length
type Length<T extends readonly unknown[]> = T['length']
Indexed access types
これまですでに、 T[number]
や T[key]
など、indexed access types には触れてきているので、今更感もありますが、インデックスによるアクセスで特定のプロパティの型にアクセスすることが可能です。今回は、タプル(というオブジェクト)が持つ length
プロパティにアクセスしてその型を得ています。
00043-exclude
type MyExclude<T, U> = T extends U ? never : T
Distributed conditional types
Distributed conditional typesとは、Genericsにユニオン型が与えられた際に、conditional typesの条件がユニオン型のそれぞれに対して適用される機能です。
今回はこの機能を用いて、 T
に渡したユニオン型のそれぞれの要素に対して、順番に T
が U
を拡張可能か確認し、拡張可能であれば never
、拡張できなければ T
を返すことで U
に含まれた型を T
から除いていきます。
00183-awaited
type MyAwaited<T extends Promise<unknown>> = T extends Promise<infer A>
? A extends Promise<unknown>
? MyAwaited<A>
: A
: never
再帰
関数だけでなく、型でも再帰を活用することが可能です。
最初、私の回答は以下のように、あくまでもテストケースをクリアするためのコードになってしまっていたのですが、回答例に載せたように再帰を使うとより深くネストされたPromiseがあっても適切に型を取り出すことが可能です。
type MyAwaited<T extends Promise<any>> = T extends Promise<infer A>
? A extends Promise<infer B>
? B
: A
: never
そのほかの要素は、先に説明したinferやconditional typesなので説明は割愛します。
00268-if
type If<C extends boolean, T, F> = C extends true ? T : F
今回活用している技術としては、これまでに説明してきたconditional typesとgenericsにおける型引数の制約だけなので、説明は割愛します。
00533-concat
type Concat<T extends unknown[], U extends unknown[]> = [...T,...U]
variadic tuple types
しれっとすでに登場しているのですが、TypeScriptにおいては配列やオブジェクトの値などを展開するときに活用するスプレッド構文を型として活用することが可能です。これはvariadic tuple typesという機能です。公式ドキュメントの中でも、concatの例が出されていました。
あとは、genericsにおける型引数の制約を活用して引数として受け取る型を配列に制限すればOKです。
00898-includes
type Includes<T extends readonly unknown[], U> = T extends [infer F, ...infer R]
? (<V>() => (V extends F ? 1 : 0)) extends (<V>() => (V extends U ? 1 : 0))
? true
: Includes<R, U>
: false
一気に難しくなった感があります。自力では解けず、TypeChallengesのソリューションからこの解答を見つけてきて、中身は、テストケースで使われている Equal
型 とやっていることが同じということはわかったのですが、関数型の部分がなぜワークしているのかの理解にかなり手こずりました。。。
要は、再帰的に配列の要素と、Uが一致するかをチェックして一致したらtrue、一致しなかったら配列の次の要素で確認。ということをしているのですが、 <V>
には何も引数を渡していないのに何が確認されているんだ。。という疑問がなかなか解消できませんでした。
(<V>() => (V extends F ? 1 : 0)) extends (<V>() => (V extends U ? 1 : 0))
この部分では、実は V
に何か引数を取って確認しているわけではなく、あくまでも F
と U
が等しいということを確認するために、関数型を作成しています。そのため、 以下のように右辺の V
を K
などに変えてもワークします。
(<V>() => (V extends F ? 1 : 0)) extends (<K>() => (K extends U ? 1 : 0))
これならば、さらに短縮して以下のように書き換えても良いのでは?と思ってしまいます。
F extends U
しかしこれだと、以下のケースで boolean
typeが入ってきた場合に boolean extends false
が成り立ってしまいダメです。
Expect<Equal<Includes<[boolean, 2, 3, 5, 6, 7], false>, false>>
そこで、 F
と U
の同一性をより厳密に比較するために回りくどい比較方法を取っているのでした。
(これ自分で思いつくことができる気がしない。。。。)
03057-push
type Push<T extends unknown[], U> = [...T, U]
なぜか急に易化。genericsにおける型引数の制約とvariadic tuple typesの組み合わせです。
03060-unshift
type Unshift<T extends unknown[], U> = [U, ...T]
Pushの逆で先頭に追加するだけです。
03312-parameters
type MyParameters<T extends (...args: any[]) => any> =
T extends (...args:infer U) => unknown
? U
: []
使う機能はこれまでと同じです。
個人的には、関数型を書くのにあまり慣れておらず、引数の型を推論する際に (...args:infer U)
と書く必要があることに気づくのが少し難しかったです。