Type Challenges(中級編)
Type Lookup
問題
実装
type LookUp<U,T> = U extends {type : T} ? U : never;
もしUにユニオン型を代入すると、ユニオン分配 になり、ユニオン型のそれぞれの要素に対して、Conditional Typeが適用されるようになる。この知識があれば解ける問題。Uがプロパティtype、値Tを持つものであるときに型を返せばいい。
Pop
問題
実装
type Pop<T extends any[]> = T['length'] extends 0 ? [] : T extends [...infer Arg, any] ? Arg : never
難易度的には中級ではあるが、初級のFirst of Arrayとほとんど同じ解き方で解ける。First Of Arrayではinferを使う解き方を提示したが、今回はその逆で最後以外の要素が知りたいので、最後以外の要素に対して inferを使い推論を行う。一つ注意点としては、テストケースとして空の配列が入力したときは空の配列を返すように求めてられているので length
を使い長いさを求めて0のときは空の配列を返却する。
Deep Readonly
問題
実装
type DeepReadonly<T> = {
readonly [k in keyof T] : keyof T[k] extends never ? T[k] : DeepReadonly<T[k]>
}
再帰的に オブジェクトのプロパティに readonly
をつけていく。ネストが深くなったとしてもreadonlyをつけていく必要があるので再帰処理を書く必要がある。プリミティブ型に対して T[k]
のようにアクセスをおこなってもプロパティを持たないので never
となる。それをもとに Conditional Types
処理を書くと実装可能である。
Get Return Type
問題
実装
type MyReturnType<T extends (...args : any[]) => any> = T extends (...args : any[]) => infer U ? U : never;
EasyのParametersに近い問題。Parametersは引数に関する型を調べる必要があったが、今回は戻り値の型を調べる必要がある。Tは関数として型制限をし、予測したい戻り値の部分にinferとして推測をおこない、その値を返すようにする。
Last of Array
問題
実装
type Last<T extends any[]> = T['length'] extends 0 ? never : T extends [...any,infer U] ? U : 0;
配列の長さが0のとき never を返す必要があるので length
プロパティで長さを調べ Conditional Types
で条件分岐。最後の型が知りたいのでそれを調べるためにinferを使う。
もしくは配列を何かしら含むときはU
を返しそれ以外のときはneverを返す
type Last<T extends any[]> = T extends [...any, infer L] ? L : never
Omit
問題
実装
type MyOmit<T, K extends keyof T> = {
[Key in keyof T as Key extends K ? never : Key] : T[Key];
}
Mapped Types
の なかで、キーの値を状況によって分岐させたい場合、型アサーションである as を使用する。
neverのものはインデックスアクセスの際にも値が取得されないので、結果的に指定したキーは除去されOmitと同じ形になる。
また、Exclude
を用いた実装も考えられる。
ただし、これだと readonly
がつくようなプロパティだとタイプエラーになる。
type MyOmit<T,U> = {
[k in Exclude<keyof T,U>] : T[k]
}
Exclude
を用いた場合、元の型Tとの関連性が維持されるわけではなく新しい型が作られたと認識してしまうと、 keyof のあとに as を続けることで元の型Tとの関連性が維持されreadonly修飾子が消えることがなく、処理をすることができる。ちなみに readonlyなどを保持したまま処理できる特性をHomomorphic Mapped Types
という。
Merge
問題
実装
type Merge<T, U> = {
[K in keyof T | keyof U] : K extends keyof U ? U[K] : K extends keyof T ? T[K] : never
};
二つのkeyofの値を |
でつないでユニオン型が作れるかがポイントとなる。あとは extends
のConditional Types
で条件分岐をして、特定のキーが含まれれば U
の方を優先して、それ以外であればTのキーでMapped Typesをおこなうようにする。エラーを防ぐためにキーが存在しない場合はneverを使うことも必要。
Diff
問題
実装
type Diff<T,U> = Omit<T & U,keyof T & keyof U>
TとUをすべて含むプロパティから TとUが共通するプロパティ名を削除すれば最終的に片方しか存在しないDiffを取得することができる。
String to Union
問題
実装
type StringToUnion<T extends string> =
T extends `${infer F}${infer R}`
? F | StringToUnion<R>
: never;
テンプレートリテラルとinferと再帰処理を組み合わせる。TypeScriptのテンプレートリテラル型において、inferを使用すると、一致する文字列の最短部分が取り出すことができる。inferを二つ続けることで最初のinferで一文字目のみを取得することができ、それ以外の処理をまた再帰処理をし最終的にUnion型でつなげることで処理が可能となる。
Tuple to Union
問題
実装
type TupleToUnion<T extends readonly any[]> = T[number]
非常にシンプルな実装である。Tは配列を期待するようにして、その配列に対してnumberを使うとユニオン型で値を取得できる。少しややこしい実装ではあるが、再帰処理とinferを組み合わせることで以下のような実装でも実現できる。
type TupleToUnion<T> = T extends [infer U,...infer P] ? U | TupleToUnion<P> : never
Trim
問題
実装
type Space = ' ' | '\t' | '\n';
type Trim<T extends string> = T extends `${Space}${infer R}` | `${infer R}${Space}` ? Trim<R> : T
テストケースでは空白以外にも改行コードやタブなどが含まれているためそれらを除去する必要がある。
String to Union
という中級の問題で文字に対して inferを使うことで最小限マッチングができる解放を紹介した。今回ポイントとなるのは、テンプレートリテラルにinfer以外の文字も普通に埋め込めることがわかるかどうかである。スペースを埋め込みスペースがあれば再帰処理をおこないそうでなければそのままの文字を返す処理をすれば最終的には得たい結果が得られるようになる。
Flatten
問題
実装
type Flatten<T extends any[]> = T extends [infer U,...infer P] ? U extends any[] ? [...Flatten<U>,...Flatten<P>] : [U,...Flatten<P>] : [];
ネストされた配列に対して、すべての配列をスプレッド展開し一次元配列にする問題。
知識としては今までのものを組み合わせることで実装可能。extends
が多くて見づらくはあるが、おこなっていることとしては、配列の特定の値が配列なのかを調べそれであればスプレッド演算子を使って再帰処理をおこなっているだけである。
Replace
問題
実装
type Replace<S extends string,FROM extends string,TO extends string> =
FROM extends ""
? S
: S extends `${infer L}${FROM}${infer R}`
? `${L}${TO}${R}` : S
S,FROM,TOともにすべて文字列であるという制約をつける。
そして置換文字が空白の場合は文字をそのまま返し、そうじゃない場合に処理をおこなう。
inferで左の文字と右の文字を推測させ、その間に置換対象文字が入っていれば処理をおこなう。
ただいinferは最小マッチングなので左右の文字がそれぞれ0文字に割り当てられることもありえる。
ReplaceAll
問題
実装
type ReplaceAll<S extends string,From extends string,To extends string> =
From extends ""
? S
: S extends `${infer L}${From}${infer R}`
? `${L}${To}${ReplaceAll<R,From,To>}`
: S
上のReplaceとほとんど同じ問題、唯一違う点としては再帰処理をおこないすべての置換対象に対して処理を行うという点のみ。Template Literal Types
のなかでもう一度typeを読み出せるか、また Template
の中なののでジャネリックにはドルマークをつけずに呼び出せる。唯一注意点をあげるとすればそこくらい。比較的easyな問題。
Capitalize
問題
実装
type MyCapitalize<T extends string> = T extends `${infer L}${infer R}` ? `${Uppercase<L>}${R}` : T;
replaceの問題とほとんど解法で解ける。唯一必要な知識としてはUppercase
を使うことだけ。
Chainable Options
問題
実装
type Chainable<T = {}> = {
option: <K extends string,V>(Key: K extends keyof T ? never : K ,value:V) => Chainable<Omit<T,K> & Record<K,V>>,
get(): T;
};
今までの問題よりも急に難易度が上がった印象。
option
は引数を受け取り、Chainable
を返す必要のある関数。get
はそのままジェネリックTを返せばいい。
まず、Tのデフォルト引数としてオブジェクトの {}
を入力する。objectと記載してもよい。
option
は、期待する入力としてキーは文字列なのでextendsで型制約をおこなう。もしTのプロパティに含まれていればneverとする。そうしてKeyの値を算出し、最終的にはOmitで新しく作りたいキーを削除したものと新しくRecordで作られたオブジェクトで &
で繋ぐことで新しいオブジェクトを作り出せる。
Append Argument
問題
実装
type AppendArgument<Fn extends (...args:any) => any,K> = Fn extends (...args:infer P) => infer R ? (...args:[...P,K]) => R : never;
今までの応用的な問題。関数型Fnは期待値として関数を受け取るのでextendsで制約を作る。
もしFnが引数をもった関数の場合スプレッド構文を使い配列のなかで展開して新しいタプルを作ることで新しい型が追加される。少し混乱した場所として
type Case1 = AppendArgument<(a: number, b: string) => number, boolean>
type Result1 = (a: number, b: string, x: boolean) => number
テストケースでいきなり x
という引数名が出てきてxなんて出てきてないと思っていたが、ここでいうx(aやbも)は任意引数であり名前はなんでも良い。テストのためにつくられた変数。typeが同じシグネチャであるかどうかは型の順番と数が重要であり、それが一致すればテストケースに合格できる。
Append to object
問題
実装
type AppendToObject<T extends {},U extends string,V> = {
[k in keyof T | U] : k extends keyof T ? T[k] : V
}
比較的簡単な問題。Mapped Types
で新しいオブジェクトを生成する。Tのプロパティと新しいキーのユニオンを生成し、そのキーがTであればオブジェクトTの値をT[k]で返し、そうじゃなければ新しい値Vを返す。
KebabCase
問題
実装
type KebabCase<S extends string> = S extends `${infer S1}${infer S2}`
? S2 extends Uncapitalize<S2>
? `${Uncapitalize<S1>}${KebabCase<S2>}`
: `${Uncapitalize<S1>}-${KebabCase<S2>}`
: S
知識としては今までの問題と代わりわなく、一つ特殊なことといえば Uncapitalize
という文字の一文字目を小文字にする Template Literal Types
を使えるかどうか。
infer S1
には文字Sの最初に一文字目、infer S2
にはそれ以降の文字が入っている。
1文字目は無条件で小文字化するので2文字目から判定しその文字が小文字であればケバブケースにして、そうじゃなければ再び再帰処理をおこない文字を連結していく。最終的にinfer S2には空文字が入るのでそこで再帰処理は完了し、そこから文字の結果が返されていくので処理は完了する。
Absolute
問題
実装
type Absolute<T extends number | string | bigint> = `${T}` extends `-${infer U}` ? U : `${T}`
これは比較的簡単な問題。気をつけることとしてTは数字しか受け取らないように型制約をおこなうこと。
Tがもし符号付きの文字であった場合にinferで文字を推論しマイナスが取り除かれた文字を返してあげればよい。
Length of String
問題
実装
type strToArray<T> = T extends `${infer _}${infer R}` ? [unknown,...strToArray<R>] : []
type LengthOfString<T> = strToArray<T>['length']
文字リテラルの長さを計算する問題。配列型にすると'length` で長さが得られるのは初級編でも実践済み。
問題はどう値を得るかというところ。
例えば Tの入力が abc
であったときの挙動をあげる。
LengthOfString<"abc">:
[unknown, ...LengthOfString<"bc">]
[unknown, unknown, ...LengthOfString<"c">]
[unknown, unknown, unknown, ...LengthOfString<"">]
🔽
[unknown, unknown, unknown]
[unknown, ...[unknown, ...[unknown, ...[]]]]
[unknown, unknown, ...[unknown, ...[]]]
[unknown, unknown, unknown, ...[]]
[unknown, unknown, unknown]
ここで鍵になるのは配列のなかでスプレッド構文を利用した再帰処理をおこなうことで最終的に配列のなかでスプレッド構文が利用された形になり、配列が得られるということである。
そうして得られた配列に対して新しいtypeを宣言し値を得る。再帰処理のときunknownをつかっているが、1としてもよいし値はなんでもよい。
Trim Left
問題
実装
type Space = ' ' | '\t' | '\n';
type TrimLeft<S extends string> = S extends `${Space}${infer P}` ? TrimLeft<P> : S
Trim とほとんど同じ解法でTrimの右方向のスペースを考慮しないだけで解ける。
AnyOf
問題
実装
type Falsy = 0 | "" | false | [] | { [K: string]: never } | undefined | null;
type AnyOf<T extends readonly any[]> = T[number] extends Falsy ? false : true;
配列の各要素に条件一致させて判定していくので numberで配列参照する。そしてテストケースに合格するためにあらゆるテストケースをtypeで定義しそれを満たしたらfalse、満たさなかったらtrueと定義する。
IsAlphabet
問題
実装
type IsAlphabet<S extends string> = Lowercase<S> extends Uppercase<S> ? false : true
いくつか実装は考えられる。解法の一つとして大文字と小文字にしたときの文字の挙動の違いを紹介する。
アルファベットは大文字小文字にすると文字が変化するが、そうでない文字はそのままの挙動であることをいかし、trueとfalseに分岐する。
Compare Array Length
問題
実装
type CompareArrayLength<T extends any[], U extends any[]> = T['length'] extends U['length'] ? 0 : keyof T extends keyof U ? -1 : 1
二つの配列の長さを比較し、文字同じ長さなら0、TがUより大きい場合は1、そうでなければ-1を返す実装。
大小記号を使って単純な比較はできないのでextendsをつかって、条件を網羅していく。
まず T['length'] extends U['length'] ? 0
は同じ値になったときの処理を記述する。
その後 keyof T extends keyof U
で配列に含まれるインデックスをユニオン型として取り出し、それが網羅されているかどうかで分岐条件を決める。
配列のkeyof出力は以下のようになるとイメージしてもらえばよい。配列はインデックスをキーに持つ特殊なオブジェクトなのでkeyofのようにプロパティを取り出そうとするとインデックスが取得されるようになる。
T = [1, 2], U = [3, 4, 5]
keyof T = 0 | 1, keyof U = 0 | 1 | 2
CheckRepeatedChars
問題
実装
type CheckRepeatedChars<T extends string, U = never> = T extends `${infer P}${infer R}`
? P extends U
? true
: CheckRepeatedChars<R,U | P>
: false
重複文字を検出する問題。解法としてはいくつか考えられるが、一つとして新しいUというジェネリックをつくり、それに過去に検索した文字列を持たせることで文字列の重複を見つけ出すという実装をおこなった。Uは初期値ではneverだが、CheckRepeatedChars<R,U | P>
再帰的に文字を呼び出すことでUには過去に検索した文字列Pが代入されていく。これを利用し、もしPがUに含まれているのであればtrueにするように実装していく。
もう一つ、Uの状態を持たせないでジェネリック型Tを使うだけで実装する方法も紹介。
type CheckRepeatedChars<T extends string> = T extends `${infer L}${infer R}` ? R extends `${any}${L}` ? true : CheckRepeatedChars<R> : false
TをL,Rに分解する。そしてRが任意の文字列とLと一致する場合は重複したことになるのでtrue、そうじゃないときは再帰処理。見つからなかったらfalseにする。個人的には下の解の方がわかりやすいが、新しくジェネリック型を定義して値を更新していくのも良く使うテクニックなので紹介した。
Reverse
問題
実装
type Reverse<T extends any[]> = T extends [infer F, ...infer Rest] ? [...Reverse<Rest>,F] : T
処理のステップとしては以下のように進んでいく。
次第にRestの配列自体が短くなっていき、空配列であればそれをそのまま返し処理が終了する。
再帰処理を使い、スプレッドで前に残りの配列、後ろに最初の数値を展開していくシンプルな実装である。
[...Reverse<[2, 3]>, 1]
[...Reverse<[3]>, 2]
[...Reverse<[]>, 3]
また、Lという初期値が空配列のジェネリックを作成し、再帰処理で Lに値を追加していき逆配列を取得することも考えられる。実装の内容としては上記の実装と同じである。
type Reverse<T extends any[],L extends any[] = []> = T extends [infer P,...infer R] ? Reverse<R,[P,...L]> : L
Number Range
問題
実装
type Utils<L,C extends any[] = [],R=L> = C['length'] extends L ? R : Utils<L,[...C,0],C['length'] | R>
type NumberRange<L,H> = L | Exclude<Utils<H>, Utils<L>>
考え方としては、0〜Lまでのユニオンと0〜Hまでの二つを作る。そしてその二つのユニオンから重複部分を削除することで、特定の範囲のユニオンを生成することができる。
Excludeしたときに、始点の値も削除されるので、L |
でその値も追加しておくことが必要。
type-challenge
では配列の範囲を作るときのテクニックとして再帰処理を使うことが多い。ある配列にスペレッドと特定の数(unknownとか0でもなんでもいい)を追加していくことで配列の長さが長くなるのでそれを利用する。
Drop Char
問題
実装
type DropChar<S, C extends string> = S extends `${infer Head}${C}${infer Tail}`
? DropChar<`${Head}${Tail}`, C>
: S;
Trim Left とかなり似た問題である。Trim Leftは左端の文字を削除する必要があったので、削除したい文字を左端に定義すればよかったが、今回は状況として間に入るケースもあるのでinferの二つで消したい文字を囲む。
そして条件を満たすものがあればそれを削除したものを再帰処理にかけ対象の文字が削除されるまで処理を続けていく。
'butter fly!' からスペースを削除する例を考える。
${infer Head}${C}${infer Tail} が以下のように分解される。
Head = 'butter'
C = ' '
Tail = 'fly!'
Trim Right
問題
実装
type Space = " " | "\n" | "\t"
type TrimRight<S extends string> = S extends `${infer P}${Space}` ? TrimRight<P> : S
Trim Left の反対の問題。解き方としてはSpaceを右に持ってくるか、左に持ってくるかだけの違い。
Without
問題
実装
type ToUnion<U> = U extends any[] ? U[number] : U
type Without<T extends any[], U> = T extends [infer P,...infer R] ? P extends ToUnion<U> ? Without<R,U> : [P,...Without<R,U>] : T
注意点としては条件 Uが整数ではなく配列として複数値渡されるテストケースがあるので、配列に対してユニオン型を生成する。そして配列のなかで一文字ずつ条件Uと一致しているかを検証し、一致していたら切り取ったものを再度処理しもし合致するものがなければ、その値は配列そして残りつつ、再帰処理をおこなっていく。
Shift
問題
実装
type Shift<T extends any[]> = T extends [infer P,...infer R] ? [...R] : []
考え方としては非常にシンプル。もしTが1つ以上に値をもつ配列である先頭の数字がP,それ以外の数字がRとなり、その残りのスプレッドで展開した辺りが結果的に先頭を取り除いた値となる。
Integer
問題
実装
type Integer<T extends number> = number extends T ? never : `${T}` extends `${infer P}.${infer U}` ? never : T
いくつか実装が考えられる。まず一つ目として、Tが数値であるかどうかを number extends T
で判定している。 Tがリテラル型の場合、numberという抽象的な型に包含されないので、falseとなり数値であるという判定になる。あとはテンプレート文字を使い数値を文字列に変換して、その文字列の間に .
があるかどうかで整数を判定する。
type Integer<T extends number> = `${T}` extends `${bigint}` ? T : never
もう一つの実装を紹介する。テンプレートリテラルのなかでbigint 型のすべての値が文字列型として展開される。もしTが整数であれば具体的な数値はすべての数値を包含する抽象的な型に包含されるため、整数値であると判断することができ、そのままTを返せばよい。
Zip
問題
実装
type Zip<T, U> = T extends [infer TL,...infer TR] ? U extends [infer UL,...infer UR] ? [[TL,UL],...Zip<TR,UR>] : [] : []
Pythonのzip関数のようなものを実装する問題。考え方としてはシンプルで、TとUというジェネリックをそれぞれ配列であるかを検証する。そしてもし両方の条件を満たせば最初の要素同士を配列として再帰処理を返していくようにする。
IsTuple
問題
実装
type IsTuple<T> = [T] extends [never]
? false
: T extends ReadonlyArray<unknown>
? number extends T['length']
? false
: true
: false
Tがタプル型であるかを判定する問題。
[T] extends [never]
はTがneverなときfalseを返すために必要な実装。T extends never
では never extends neverのとき左側のneverはないものとして扱うためfalseの条件分岐には到達しない。そのため[]
をつけてT 全体を1つの型として扱い、比較をおこなう。
T extends ReadonlyArray<unknown>
これはTがreadonly型の配列であるかを判定している。
number extends T['length']
、ここでTがタプルの場合具体的な値を持つためfalseとなりつまりこれがタプルということになり、Tが配列の場合はnumberとなるため、number extends T['length']
がtrueとなり、falseのほうに条件分岐する。応用力が問われる問題。
ちなみに、ReadonlyArray<unknown>
は readonly unknown[]
と書いても問題ない。
Trunc
問題
実装
type Trunc<T extends number | string> = `${T}` extends `${infer P}.${any}` ? (`${P}` extends "-" | "" ? `${P}0` : `${P}`) : `${T}`
Tを小数点抜きの文字に変換するコード。基本的には ${infer P}.${any}
のコードで小数点を見つけ出すのが肝。ただし、テストケースに合格するためには .3
や -3.2
など .
の前に記号や数字がない場合も検出しなければならない。コードを追加し、そのような場合でも 0
が返るようにしている。
OmitByType
問題
実装
type OmitByType<T, U> = {
[K in keyof T as T[K] extends U ? never : K] : T[K]
}
解法としてはOmitとほとんど同じで、Omitはプロパティ名から削除するものを選択するのに対してこの問題は、バリューの値をもとにして削除する対象を決める。
なので asのあとにKではなく、T[K]
を指定し該当する値が存在するかどうかを判定する必要がある。
All
問題
実装
type All<T extends any[],F> = T extends [infer P,...infer U] ? Equal<F,P> extends true ? All<U,F> : false : true
実装方法としては配列の一つ一つを比較して、その値が正しければtrueを返し続ける処理をおこなう。最後配列が空になればtrueとなり、全ての配列に対して値が同じ配列であることを確認できたことを示している。Equal
に関しては type-challenges
で使われている等価比較の型が用意されているので使用する。
import type { Equal, Expect } from '@type-challenges/utils'
Parse URL Params
問題
実装
type ParseUrlParams<T extends string> = T extends `${string}:${infer S}`
? S extends `${infer P}/${infer R}`
? P | ParseUrlParams<R>
: S
: never
URLのパラメータ部分で特定の形式の文字を取得し合致するものがあればユニオンとして接続していく問題。実装としては非常にシンプル。もし合致するものがあれば P | ParseUrlParams<R>
で新しく発見された文字に連結していきながら再帰処理をおこなっていく。
IsNever
問題
実装
type IsNever<T> = [T] extends [never] ? true : false
この問題のキーとしては、neverを評価する際にタプルにすること。
never
は 要素が存在しない
ことを意味する型なので、分解の結果として条件型自体が評価されることなく never
が返されます。
そのため、タプルで括ることで、T 全体が 1 つの要素として扱われて、ユニオン型として分解されずにそのまま値を評価することができる。
Flip
問題
実装
type Flip<T extends Record<string,string | boolean | number>> = {
[k in keyof T as `${T[k]}`] : k
}
オブジェクトのキーとバリューを入れ替える問題。
[k in keyof T as `${T[k]}`] : k
自体の記述は as
の使い方も含めて過去に出てきたので難しくはないかと思う。ここでのキーになるのは、Record
。T[k]
のときキーになりうる値のみを制限することでエラーになることを防ぐ。ここでTはstringがキーのstring | boolean | number
が値のオブジェクトとして入力値を制限することで、エラーが発生するのを防ぐ。
StartsWith
問題
実装
type StartsWith<T extends string, U extends string> = T extends `${U}${infer P}` ? true : false
中級になっているが、レベル的には初級。前方にUを含む文字があるか、残りをinferで推論する。
もし含まれていればtrueを返し、そうでなければfalseを返す。
EndsWith
問題
実装
type EndsWith<T extends string, U extends string> = T extends `${infer P}${U}` ? true : false
先ほどの StartsWith
の逆。Uがあとに来る文字列かどうかを調べればよい。
Filter
問題
実装
type Filter<T extends any[], P> = T extends [infer L,...infer R] ? L extends P ? [L,...Filter<R,P>] : Filter<R,P> : []
配列が条件に含まれているか確認。配列の一つ一つを調べていき、合致するなら結果に加えて、そうでなければ空の配列を返すようにする。