github octcat icon
twitter bird icon
rss icon

Type<Challenge[]>~easy?~

2022-06-05

puzzle

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] };

Playground_00004_pick

完全に初めてこれを解いたときには、「本当に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

これを今回の回答に適用すると、例えば TPerson型だった場合、以下状態と解釈できます。

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の第二引数に定義されていますが、 schooltypeof obj1 に含まれないキーなので、extendsで弾かれてエラーになります。

00007-readonly

TypeScript組み込みの Readonly<T> を実装していきます。

最終的なコードは以下です。

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

Playground_00007_readonly

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 anyany のキー値、つまり 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

上のコードのように複数回同じ値をキーに取った場合にはエラーになります。しかしこの挙動は stringnumber でも同じだろ。と思っているのですが、試しに解答を以下のコードに変えてみるとエラーになります。

//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']

Playground_00018_tuple_length

Indexed access types

これまですでに、 T[number]T[key] など、indexed access types には触れてきているので、今更感もありますが、インデックスによるアクセスで特定のプロパティの型にアクセスすることが可能です。今回は、タプル(というオブジェクト)が持つ length プロパティにアクセスしてその型を得ています。

00043-exclude

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

Playground_00043_exclude

Distributed conditional types

Distributed conditional typesとは、Genericsにユニオン型が与えられた際に、conditional typesの条件がユニオン型のそれぞれに対して適用される機能です。

今回はこの機能を用いて、 T に渡したユニオン型のそれぞれの要素に対して、順番に TU を拡張可能か確認し、拡張可能であれば 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

Playground_00183_awaited

再帰

関数だけでなく、型でも再帰を活用することが可能です。

最初、私の回答は以下のように、あくまでもテストケースをクリアするためのコードになってしまっていたのですが、回答例に載せたように再帰を使うとより深くネストされた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

Playground_00268_if

今回活用している技術としては、これまでに説明してきたconditional typesとgenericsにおける型引数の制約だけなので、説明は割愛します。

00533-concat

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

Playground_00533_concat

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

Playground_00898_includes

一気に難しくなった感があります。自力では解けず、TypeChallengesのソリューションからこの解答を見つけてきて、中身は、テストケースで使われている Equal 型 とやっていることが同じということはわかったのですが、関数型の部分がなぜワークしているのかの理解にかなり手こずりました。。。

要は、再帰的に配列の要素と、Uが一致するかをチェックして一致したらtrue、一致しなかったら配列の次の要素で確認。ということをしているのですが、 <V> には何も引数を渡していないのに何が確認されているんだ。。という疑問がなかなか解消できませんでした。

(<V>() => (V extends F ? 1 : 0)) extends (<V>() => (V extends U ? 1 : 0))

この部分では、実は V に何か引数を取って確認しているわけではなく、あくまでも FU が等しいということを確認するために、関数型を作成しています。そのため、 以下のように右辺の VK などに変えてもワークします。

(<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>>

そこで、 FU の同一性をより厳密に比較するために回りくどい比較方法を取っているのでした。

(これ自分で思いつくことができる気がしない。。。。)

03057-push

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

Playground_03057_push

なぜか急に易化。genericsにおける型引数の制約とvariadic tuple typesの組み合わせです。

03060-unshift

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

Playground_03060_unshift

Pushの逆で先頭に追加するだけです。

03312-parameters

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

Playground_03312_parameters

使う機能はこれまでと同じです。

個人的には、関数型を書くのにあまり慣れておらず、引数の型を推論する際に (...args:infer U) と書く必要があることに気づくのが少し難しかったです。