type challenges 【中級編】
type challenge
https://github.com/type-challenges/type-challenges の mediumを1日1つくらいやっていく
ReturnType
type MyReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer U ? U : never
MyReturnType<T extends (...args: any[]) => any>
だけ書ければ後はそのまま
Omit
type MyOmit<T, K extends keyof T> = {
[k in keyof T as k extends K ? never : k]: T[k]
}
初めas k
が抜けてて
[k in keyof T extends K ? never : k]: T[k]
と書いていてType parameter 'k' has a circular constraint.
というエラーが発生
kがvalueなのに型として使ってしまっていたのでas kを追加
Readonly2
最初デフォルト値の指定が分からず調べた
type MyReadonly2<T, K extends keyof T | null = null> = K extends keyof T ? Readonly<Pick<T, K>> & Omit<T, K> : Readonly<T>
こんなやつを書いたが通らず。(実装的には多分正しいが型チェック関数的に通らなかった)
無駄に初期値をnullにする必要はなく、keyof Tで良かった
type MyReadonly2<T, K extends keyof T = keyof T> = Readonly<Pick<T, K>> & Omit<T, K>
DeepReadonly
type isObject<T> = T extends () => void ? false : T extends object ? true : false
type DeepReadonly<T> = {
readonly [P in keyof T]: isObject<T[P]> extends true ? DeepReadonly<T[P]> : T[P]
}
オブジェクトのみという制約だったので、こんなんでいけた。
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P]> extends object ? DeepReadonly<T[P]> : T[P]
}
最初シンプルにextends objectとしていたが、関数もobjectとして判定されてしまってたので関数の場合はfalseになるisObjectを作成
TupleToUnion
問題
タプルをユニオンに変換
type Arr = ['1', '2', '3']
type Test = TupleToUnion<Arr> // expected to be '1' | '2' | '3'
解答
type TupleToUnion<T extends any[]> = T[number]
メモ
配列からユニオンを生成するテンプレを覚えていたのでいけた
const arr = [1, 2, 3] as const
type hoge = typeof arr[number]
Chainable Options
問題
チェインを作成
try
難しかった。。
最初こんな感じで書いた
type Chainable<T extends object = {}> = {
option(key: string, value: any): Chainable<{key: typeof value} & T>
get(): T
}
これだとkey, valueが型として扱われず、
{ key: any } & { key: any} & {key: any}
のようになってしまった。
key, valueを型として扱うという発想が出てこず、諦めた。
解答
type Chainable<T extends object = {}> = {
option<K extends string, V extends any>(key: K, value: V): Chainable<Record<K, V> & Omit<T, K>>
get(): T
}
途中までは良さげだった。
key, valueを型として扱うために関数の先頭にジェネリクスを付ける必要があった。(この発想がなかった)
勉強になった。Recordも覚えておく。
これだとエラー発生箇所を考慮できていなかった。
type Chainable<T extends object = {}> = {
option<K extends string, V extends any>(key: K extends keyof T ? never : K, value: V): Chainable<Omit<T, K> & Record<K, V>>
get(): T
}
が正しい
Last of Array
配列の最後のtypeを取得
type Last<T extends any[]> = T extends [infer U, ...infer V] ? V extends [] ? U : Last<V> : never
これで通った。
解答見たらもっと短くいけたみたい。
type Last<T extends any[]> = T extends [...any[], infer U] ? U : never
スプレッド構文で最後の要素とそれ以外も取得可能。これを使えば簡潔に書ける
Pop
配列の最後の要素を除いた配列を取得
Last of Arrayとほぼ一緒だったので一瞬だった
type Pop<T extends any[]> = T extends [...infer U, any] ? U : []
Promise.all
type AwaitedAll<T extends readonly any[]> = T extends readonly [infer U, ...infer V] ? [Awaited<U>, ...AwaitedAll<V>] : []
declare function PromiseAll<T extends readonly any[]>(values: T): Promise<AwaitedAll<T>>
こんなものを思いついたが、テストケース4つのうちas constを使っていない2つで弾かれた。
原因はPromiseAll<T extends readonly any[]>と
Tの型をreadonlyにしてしまっていたこと。ここではTはreadonlyにする必要はなく、valuesだけreadonlyにする必要があった。
valuesだけreadonlyにする場合以下のように書くことができる
type AwaitedAll<T extends any[]> = T extends [infer U, ...infer V] ? [Awaited<U>, ...AwaitedAll<V>] : T
declare function PromiseAll<T extends any[]>(values: readonly [...T]): Promise<AwaitedAll<T>>
ただ、これでも最後のテストケースで失敗する。
declare function PromiseAll<T extends any[]>(values: readonly [...T]):
Promise<{ [K in keyof T]: T[K] extends Promise<infer R> ? R : T[K] }>;
これでいけるみたい。
readonly [...T]
という表現に馴染みがない。(上級編でも見かけなかった気がする)
LookUp
最初↓で良い感じに分配されていけるのではないかと思ったがダメだった
type LookUp<U extends {type: string}, T extends string> = U['type'] extends T ? U : never
次にUnionをinferで分解しようと思って↓を書いたら通った。
type LookUp<U extends {type: string}, T extends string> = U extends infer V | infer W ? V extends {type: string} ? V['type'] extends T ? V : never : never : never
結構無駄なことをしている気がして解答を見たら最初の案の方向性でいけたらしい。ただし自分の最初の書き方だとダメみたい。
type LookUp<U, T> = U extends {type: T} ? U : never
extends {type: T}
がミソ。
Trim left
文字列の左端の空白文字を削除する
最初どうすれば良いか分からず、試しに文字列でもinfer使えるんじゃね?というノリで使ってみたところ${}
で囲めば使えた。
type TrimLeft<S extends string> = S extends ` ${infer U}` ? TrimLeft<U> : S
また、スペースだけでなく\n
,\t
もエスケープする必要があった。
三項演算子で各エスケープ条件を繋げていけばいけそう!→でも3回三項演算子使うの面倒...→試しにunion使っていけないかな?→いけた
type TrimLeft<S extends string> = S extends `${' ' | '\n' | '\t' }${infer U}` ? TrimLeft<U> : S
だいぶ成長を感じた。
Trim
TrimLeftとほぼ一緒。両端から空白文字を除外する。
TrimLeft, TrimRightを定義してやればいけると考え、それでいけた。
type TrimLeft<S extends string> = S extends `${' ' | '\n' | '\t'}${infer T}` ? TrimLeft<T> : S
type TrimRight<S extends string> = S extends `${infer T}${' ' | '\n' | '\t'}` ? TrimRight<T> : S
type Trim<S extends string> = TrimLeft<TrimRight<S>>
ただ、ちょっと冗長である。
もっと簡潔に書いている人がいた。
type space = " " | "\n" | "\t"
type Trim<S extends string> = S extends `${space}${infer T}` | `${infer T}${space}` ? Trim<T> : S;
こちらの方が簡潔で良い。extendsの後にUnionを持ってくる発想はなかった。
Capitalize
文字の1文字目を大文字化するやつ。
知らなかったがユーティリティ型に存在しているらしい。
初めはtypescriptで大文字化とかできるの??って感じだったがとりあえず空文字(''
)だけ通るようにしようと思って↓を書いてみた。
type MyCapitalize<S extends string> = S extends `${infer T}${infer U}` ? hoge : S
無事空文字は通った。
次にtypescriptで大文字化するような機能はないか調べてみた。
そしたらCapitalizeというユーティリティ型の他にUppercaseというユーティリティ型も存在することを発見した。
試しに使ってみたところ、通った。
type MyCapitalize<S extends string> = S extends `${infer T}${infer U}` ? `${Uppercase<T>}${U}` : S
Uppercase使っちゃって良いのかは疑問だったが、他の解答見てたところ使っていたので大丈夫みたい。
Replace
文字列Sに含まれる文字FromをToに一度だけ置き換える。
一度だけで良いのがポイント。
inferを使えば簡単にできる。infer使うと空文字も該当してしまうので空文字は除外する必要があった。
type Replace<S extends string, From extends string, To extends string> = From extends '' ? S : S extends `${infer T}${From}${infer U}` ? `${T}${To}${U}` : S
ReplaceAll
文字列Sに含まれる文字列Fromを全てToに置き換える
Replaceを再帰的に行えばいける
type ReplaceAll<S extends string, From extends string, To extends string, prev extends string = ''> = From extends '' ? S : S extends `${prev}${infer T}${From}${infer U}` ? ReplaceAll<`${prev}${T}${To}${U}`, From, To, `${prev}${T}${To}`> : S
↑のように実装した。
なんか無駄に頑張っている気がして解答見たらもっと良いのがあった。
type ReplaceAll<S extends string, From extends string, To extends string> = From extends '' ? S : S extends `${infer T}${From}${infer U}` ? `${T}${To}${ReplaceAll<U, From, To>}` : S
ReplaceAllを文字列中で使うのがポイント。
Append Argument
与えられた関数型 Fn と任意の型 A に対して、第一引数に Fn を取り、第二引数に A を取り、Fn の引数に A を追加した関数型 G を生成
関数をinferするのどうやってやるか忘れていた。ReturnTypeやった時の記録を見て思い出した。
↓の感じでinferできる
Fn extends (...args: infer T) => infer U
↑をもとに素直に書いたらいけた。
type AppendArgument<Fn extends (...args: any[]) => any, A> = Fn extends (...args: infer T) => infer U ? (...args: [...T, A]) => U : never
Permutation
Union 型を Union 型の値の順列を含む配列に変換
↓のような変換をする
type perm = Permutation<'A' | 'B' | 'C'>; // ['A', 'B', 'C'] | ['A', 'C', 'B'] | ['B', 'A', 'C'] | ['B', 'C', 'A'] | ['C', 'A', 'B'] | ['C', 'B', 'A']
解答
かなり難しかった。
問題をパッと見た時まずUnion distributionをうまく使うんだろうな〜と思った。
type Permutation<T>
があったとき、Tが Unionだったら要素を一つ取り出したものと残りの要素を再帰的にPermutateしたもののリストを返し、Unionじゃなかったら[T]
を返すみたいにすればいけるのでは?と考えた。
TがUnionであることってどうやって判定するんだ??となったが、たまたまこの前Unionの判定をやっている記事を読んでいたのを思い出した。
この記事の内容がまさにドンピシャで、この記事をもとに↓のような解答を作成した。
type Permutation<T, U = T> = T extends U ? [U] extends [T] ? [T] : [T, ...Permutation<Exclude<U, T>>] : never
ポイントはunion distributionが発生するのは型変数に union を渡して、かつその union が条件分岐に利用される場合だということ。
この場合Tは分配されるがUは分配されずunionのままである。
上記解答でテストケース4つ通った。
neverを渡された時だけ通っていなかった。
never無視すりゃええんか。簡単やな。これでいけるやろ↓
type Permutation<T, U = T> = T extends never ? [] : T extends U ? [U] extends [T] ? [T] : [T, ...Permutation<Exclude<U, T>>] : never
これだといけなかった、、、
Permutation<never>
がどうしてもnever
になってしまった。
type A = never extends never ? true : false
でtype A = true
になるのに何で??って感じだったが調べたらそういう性質らしい。
neverは空のunionとして扱われるよう。
T[] extends never[]
や[T] extends [never]
と書くと良いみたい。
最終的に↓のコードで前テストケース通った。めでたしめでたし。めっちゃ勉強になった。
type Permutation<T, U = T> = T[] extends never[] ? [] : T extends U ? [U] extends [T] ? [T] : [T, ...Permutation<Exclude<U, T>>] : never
追記
後から解答見たがPermutation<never> = []
になっているので↓でいけるみたい
type Permutation<T, U = T> = T[] extends never[] ? [] : T extends any ? [T, ...Permutation<Exclude<U, T>>] : never
Length of string
文字列の長さを計算する
解答
配列の長さがArray["length"]
でとれることを覚えていたので文字列を配列に変換→変換し終わったら長さを計算でいけると思った
この方針をそのまま実装してみたら通った
type LengthOfString<S extends string, Past extends string[] = []> = S extends "" ? Past['length'] : S extends `${infer Head}${infer Rest}` ? LengthOfString<Rest, [...Past, Head]> : never
第二引数に配列に変換中の文字列を受け取れるようにして、変換終わるまでは再帰的に呼ぶようにした。
別解
解答見たら文字列を配列に変換する型を作成している人がいた
type StringToArray<S extends string> = S extends `${infer F}${infer R}` ? [F, ...StringToArray<R>] : [];
type LengthOfString<S extends string> = S extends `` ? 0 : StringToArray<S>[`length`];
こちらの方がやっていることが分かりやすくて良い。無駄に第二引数とかもないし。
Flatten
配列を一次元配列に変換
解答
配列の中身を取り出して、取り出した中身が配列だったら再帰的にFlattenを呼び出し、配列でない場合は取り出した要素を返す みたいにすればいけると考えた。
type Flatten<T extends any[]> = T extends [infer Head, ...infer Rest] ? Head extends any[]? [...Flatten<Head>, ...Flatten<Rest>] : [Head, ...Flatten<Rest>] : []
その考えをそのまま実装したら通った。
Append to object
object, key, valueを受け取ってobjectにkey, valueを追加する
解答
初め、シンプルに
type AppendToObject<T, U extends string, V> = T & {U, V}
でいけると思ったが、これだと{U, V}
のUが型ではなくシンプルに値になってしまうのでU
というkeyになってしまいダメだった。
代わりにRecordを使って
type AppendToObject<T, U extends string, V> = T & Record<U, V>
としたところ、Uが型として認識されたがEqualとは判定されなかった(型チェック関数的に通らなかっただけの気がする)
その後Mapped typeを試してみたらすんなり行けた
type AppendToObject<T, U extends string, V> = {
[K in keyof T | U]: K extends keyof T ? T[K] : V
}
Absolute
正の数を出力
解答
文字列に変換して先頭に-
がついていたら外せばよさそうだった
type Absolute<T extends number | string | bigint> = `${T}` extends `-${infer U}`? U : `${T}`
そのままの感じで実装したらあっけなく通った
この辺の文字列変換のものであれば大体通るようになった
String to Union
文字列をUnionに変換する
解答
先頭から1文字ずつ取り出して再帰的にUnionでくっつけていけばいけそうだった。
type StringToUnion<T extends string> = T extends `${infer Head}${infer Rest}` ? Head | StringToUnion<Rest> : never
上記方針でそのままあっけなく通った。
Merge
2つのobjectをマージする。共通プロパティが存在する場合は第2引数のオブジェクトのプロパティが優先される。
解答
数日前にやったAppend to objectと同じようにMapped typeでいける考えて、そのまま実装したら通った。
type Merge<F, S> = {
[k in keyof F | keyof S] : k extends keyof S ? S[k] : k extends keyof F ? F[k] : never
}
第2引数のオブジェクトのプロパティを優先するために先にS[k]
を参照しているのがポイント
KebabCase
キャメルケースもしくはパスカルケースの文字列を、ケバブケースに置換
解答
大文字かを判定して大文字だったらハイフン+小文字に変換すればいけそうと考えた。
ただし文字列の先頭の場合はハイフンはつけないように注意する
type KebabCase<S extends string, isHead extends boolean = true> = S extends `${infer Head}${infer Rest}`
? Head extends Lowercase<Head>
? `${Head}${KebabCase<Rest, false>}`
: isHead extends true
? `${Lowercase<Head>}${KebabCase<Rest, false>}`
: `-${Lowercase<Head>}${KebabCase<Rest, false>}`
: S
上記方針をもとに実装した最終的な解答こんな感じ。
初めUppercase
を使ってT
とUppercase<T>
が一致するかどうかで大文字かどうかを判定していたが、それだと-
などの記号が大文字と判定されてしまうのでLowercase
で判定するようにした。
また文字列の先頭かどうかを判定するために第二引数にisHeadという引数を作って最初だけtrueになるようにした。
条件分岐が若干煩雑だが自力解答できて嬉しみ
別解
解答見たらもっとスマートなやつがあった。
type KebabCase<S extends string> = S extends `${infer First}${infer Rest}`
? Rest extends Uncapitalize<Rest>
? `${Lowercase<First>}${KebabCase<Rest>}`
: `${Lowercase<First>}-${KebabCase<Rest>}`
: S
文字列は全て小文字にして良いのでFirstに関しては常にLowercaseで小文字に、2文字目以降で大文字が出てきた際にハイフンを前につければ良いのでRestの先頭の文字が大文字かどうかをUncapitalizeを使って判
定し、大文字の場合はハイフンをつけている。
自分の解答より条件分岐が1つ少なくてスマート。
先頭の文字を小文字化するUncapitalize覚えてなかったので覚えておく。
上級のSnakeCaseも全く同じ要領でできた
type SnakeCase<T extends string> = T extends `${infer Head}${infer Rest}`
? Rest extends Uncapitalize<Rest>
? `${Lowercase<Head>}${SnakeCase<Rest>}`
: `${Lowercase<Head>}_${SnakeCase<Rest>}`
: T
同じく上級のCamelCaseもほぼ同じだった
type CamelCase<S extends string, isHead extends boolean = true> = S extends `${infer Head}${infer Rest}`
? Rest extends `_${infer _Rest}`
? `${Head}${CamelCase<Capitalize<_Rest>, false>}`
: isHead extends true
? `${Lowercase<Head>}${CamelCase<Uncapitalize<Rest>, false>}`
: `${Head}${CamelCase<Uncapitalize<Rest>, false>}`
: S
Diff
二つのオブジェクトで非共通のプロパティを抽出
解答
前に考えたことがあったので一瞬だった
type Diff<O, O1> = Omit<O & O1, keyof O & keyof O1>
別解
先にOとO1のUnionを取る手もある
type Diff<O, O1> = Omit<O & O1, keyof O | O1>
AnyOf
Trueとなる要素が含まれているかリスト中にどうかを判定
解答
trueとなる要素はたくさんありすぎて判定が難しいので、falseとなる要素を集めてFalseの判定を行い、falseでない=trueである要素が含まれていたらtrueを返す というような流れでいけると考えた
この方針を実装した最終的な解答が以下
type FalsiveValues = [0, '', false, [], {}, undefined, null]
type Includes<T extends readonly any[], U> = T extends [infer First, ...infer Rest]
? Equal<First, U> extends true
? true
: Includes<Rest, U>
: false
type AnyOf<T extends readonly any[]> = T extends [infer First, ...infer Rest]
? Includes<FalsiveValues, First> extends true
? AnyOf<Rest>
: true
: false
falseとなる要素を集めたリストを作成してこのリストに含まれているかどうかでfalseかどうかを判定している
初め
type Falsive = 0 | '' | false | [] | {} | undefined | null
type AnyOf<T extends readonly any[]> = T extends [infer First, ...infer Rest]
? First extends Falsive
? AnyOf<Rest>
: true
: false
としていたがFirst extends Falsive
の判定がうまくいかなかった。そのため無駄にリストにしてリストに含まれているかどうかで判定した。
別解
First extends Falsive
の方向性も間違っていなかったみたい。
空のオブジェクトの書き方がよくなかったよう。
type Falsive = 0 | '' | false | [] | Record<string, never> | undefined | null
type AnyOf<T extends readonly any[]> = T extends [infer First, ...infer Rest]
? First extends Falsive
? AnyOf<Rest>
: true
: false
のように{}
ではなく、Record<string, never>
にすると通る。
IsNever
neverを判定
解答
Permutationで既にやった。neverをそのまま判定できないがリストにすると判定できる
type IsNever<T> = [T] extends [never] ? true : false
IsUnion
Union型を判定
解答
これもPermutationの時に学んだのを覚えていた。
Union distributionが条件判定の際に発生するのでわざと発生させて、分配前後で値が異なっているかどうかを見ることでUnionかそうでないかを判定する
type IsNever<T> = [T] extends [never] ? true : false
type IsUnion<T, U = T> = IsNever<T> extends true
? false
: U extends T
? [T] extends [U]
? false
: true
: never
neverが与えられた際にfalseを返すためにIsNeverを使用した。
ReplaceKey
ユニオン、ユニオン型のkey、Replace後のオブジェクトを受け取ってユニオンのReplaceをするReplaceKeysを実装
例
type ReplacedNodes = ReplaceKeys<Nodes, 'name' | 'flag', {name: number, flag: string}> // {type: 'A', name: number, flag: string} | {type: 'B', id: number, flag: string} | {type: 'C', name: number, flag: string} // would replace name from string to number, replace flag from number to string.
type ReplacedNotExistKeys = ReplaceKeys<Nodes, 'name', {aa: number}> // {type: 'A', name: never, flag: number} | NodeB | {type: 'C', name: never, flag: number} // would replace name to never
解答
Union distributionを適切に扱えるかどうかが試されている問題
Uは最初にdistribution発生してほしいのでU extends U
でdistributionを発生させる。
Tはなるべくdistributionしてほしくない(上の方でdistributionするとオブジェクトがたくさん連なったUnionになってしまう)。条件に使うとどうしてもdistributionが発生するので下の方で条件として使用してfalseの場合はneverを返すようにする。こうするとprimitive | never => primitive
になる。
最終的な解答は以下
type ReplaceKeys<U, T, Y> = U extends U
? {
[k in keyof U]: k extends keyof Y
? Y[k]
: k extends T
? never
: U[k]
}
: never
別解
解答見たらunion distributionを意識せずとも解けるようだった。
type ReplaceKeys<U, T, Y> = {
[k in keyof U]: k extends T
? k extends keyof Y
? Y[k]
: never
: U[k]
}
こんなんでいけるらしい。Uは自動的にdistributionされるっぽい。Mapped typeでもdistributionは発生するらしい。
また、Tはちゃんとneverが返るようにすれば良いみたい。
Remove Index Signature
オブジェクトからIndex signatureを削除
解答
難しかった。全く思い浮かばず久しぶりに自力正解できなかった
こんな感じで一つ一つstring, number, symbolを判定していくと良いらしい
type RemoveIndexSignature<T> = {
[key in keyof T as string extends key ? never : number extends key ? never : symbol extends key ? never : key]: T[key]
}
Percentage parser
/^(\+|\-)?(\d*)?(\%)?$/
の正規表現に従う文字列をparseしてリストで返す問題
解答
まず先頭の文字に来る+ | -
をparseして、その後最後に来る%
をparseした
type SuffixParser<A extends string> = A extends `${infer Rest}%` ? [Rest, '%'] : [A, '']
type PercentageParser<A extends string> = A extends `${infer First}${infer Rest}`
? First extends '+' | '-'
? [First, ...SuffixParser<Rest>]
: ['', ...SuffixParser<A>]
: ['', '', '']
DropChar
文字列から指定された文字を除外
解答
1文字ずつ見ていって、指定された文字をextendしているか素直に判定していったら通った
type DropChar<S extends string, C extends string> = S extends `${infer First}${infer Rest}`
? First extends C
? DropChar<Rest, C>
: `${First}${DropChar<Rest, C>}`
: ''
PickByType
typeを指定して、objectからtypeに合致するプロパティを取り出す
解答
Mapped typeを使ってプロパティが指定されたtypeをextendsしているかを判定すれば良い
type PickByType<T, U> = {
[k in keyof T as T[k] extends U ? k : never]: T[k]
}
as T[k]
がポイント
StartsWith
指定された文字列から始まるかどうかを判定
解答
そのまま
type StartsWith<T extends string, U extends string> = T extends `${U}${any}` ? true : false
EndsWith
指定された文字列で終わるかどうかを判定
解答
StarsWithと同じくそのまま
type EndsWith<T extends string, U extends string> = T extends `${any}${U}` ? true : false
PartialByKeys
PartialByKeys<T extends object, K extends keyof T>
が与えられたときにプロパティKをオプショナルにする
解答
type PartialByKeys<T, K extends keyof T= keyof T> = {
[k in keyof T as k extends K ? k : never]? : T[k]
} & {
[k in keyof T as k extends K ? never: k] : T[k]
}
こんな感じにしたが通らずだった。
オプショナルにした結果{ k: T[k] | undefined }
になってしまうのが原因かと思ったが違った。
解答見たらオブジェクトを一つにまとめるMergeが必要だった。
Mergeがあれば中身はもっと省略してよかった。
type Merge<T> = {
[k in keyof T]: T[k]
}
type PartialByKeys<T, K extends keyof T= keyof T> = Merge<Partial<Pick<T, K>> & Omit<T, K>>
RequiredByKeys
Keyで指定されたプロパティをRequiredにする
解答
PartialByKeysとほぼ同じ。RequiredとOptionalの違い。
type RequiredByKeys<T, K extends keyof T = keyof T> = Merge<Required<Pick<T, K>> & Omit<T, K>>
Mutable
objectのプロパティをreadonlyでなくmutableにする
解答
-readonly
でreadonlyを外せることを知っていたのでそれを使った。
type Mutable<T extends object> = {
-readonly[k in keyof T]: T[k]
}
Mapping modifiersという機能らしい。
OmitByType
指定されたTypeのプロパティをOmit
解答
PickByTypeと同じ
type OmitByType<T, U> = {
[k in keyof T as T[k] extends U ? never : k]: T[k]
}
ObjectEntries
objectを受け取り、objectの各パラメータとパラメータの型のリストのユニオンを作成(例 ['name', string] | ['age', number]
)
解答
objectのキーを取る方法が最初思いつかなかった。単純にtype ObjectEntries<T extends object> = k in keyof T
のようなことはできなかった。
他の問題でやったように第二引数を使ってkeyof Tを初期値に与えることでkeyが変数として扱えることに気づいた。
これを踏まえて最初以下のような解答にした。
各keyごとにユニオンを取るためにunion distributionを発生させている。
type ObjectEntries<T extends object, U extends keyof T = keyof T> = U extends U? [U, T[U]] : never
これだとオプショナル引数のプロパティにundefinedが含まれてしまいダメだった。
undefinedを打ち消すNonUndefinedを導入してこれに対処した。
最終的な解答は以下。
type NonUndefined<T> = Equal<T, undefined> extends true ? undefined : Exclude<T, undefined>
type ObjectEntries<T extends object, U extends keyof T = keyof T> = U extends U? [U, NonUndefined<T[U]>] : never
Shift
配列から先頭の要素を抜いて返す
解答
特筆すべき点なし
type Shift<T extends any[]> = T extends [any, ...infer U] ? [...U] : []
Tuple to Nested Object
type a = TupleToNestedObject<['a'], string> // {a: string}
type b = TupleToNestedObject<['a', 'b'], number> // {a: {b: number}}
type c = TupleToNestedObject<[], boolean> // boolean. if the tuple is empty, just return the U type
こんな感じの型を作る
解答
先頭の要素を取り出して、先頭の要素と残りの要素を再帰的にTupleToNestedObject
したもののタプルを作ればいけた。
type TupleToNestedObject<T extends unknown[], U> = T extends [infer V, ...infer W]
? V extends string
? Record<V, TupleToNestedObject<W, U>>
: never
: U
Reverse
配列の順序を逆順にする
解答
先頭から取り出して最後に入れるという操作を再起的に行えば良い
type Reverse<T extends unknown[]> = T extends [infer U, ...infer V] ? [...Reverse<V>, U] : []
Flip Arguments
関数の引数の型の順序を逆順にする。このとき引数の順序自体は逆順にしない
解答
関数の型をgenericsで表現するやり方を若干忘れていた。(..args: any[]) => any
でいけることを思い出した。
関数の引数が取れたら、後は引数の型を1つ前のReverseで逆順にしてやるだけでいけた。
type Reverse<T extends unknown[]> = T extends [infer U, ...infer V] ? [...Reverse<V>, U] : []
type FlipArguments<T extends (...args: any[]) => any> = T extends (...args: infer U) => infer V
? (...args: Reverse<U>) => V
: never
MinusOne
1引いた数を返す
自分の解答
配列の長さを使って良い感じにできないかと思い↓のような解答を作成した。
type MinusOne<T extends number, A extends any[] = []> = [...A, any] extends { length: T} ? A['length'] : MinusOne<T, [...A, any]>
これだとある程度大きい数字になると再帰が深すぎてダメ&マイナスになる場合にダメだった。
他の人の解答
文字列として扱って頑張って9だったら8に, 8だったら7にとかを愚直にやっていた。
実装は省略。
FlattenDepth
指定された数だけFlattenする
自分の解答
数をどんどん引いていって、指定された数になったら再帰を止めればいけそうだと思った
数を引くために前にやったMinusOneの実装を持ってきた。
type MinusOne<T extends number, A extends any[] = []> = [...A, any] extends { length: T } ? A['length'] : MinusOne<T, [...A, any]>
type FlattenDepth<T, U extends number = 1> = U extends 0
? T
: T extends [infer Head, ...infer Rest]
? Head extends unknown[]
? [...FlattenDepth<Head, MinusOne<U>>, ...FlattenDepth<Rest, U>]
: [Head, ...FlattenDepth<Rest, U>]
: T
解説
ほとんど方針は同じだがMinusOneを使わずにCountしていくやり方で正解している人が多かった。MinusOneだと再帰の数で怒られたが、これだとFlattenし終わったら途中で再帰が終了する(指定された数だけ再帰を繰り返すわけではない)ので問題ない。
type FlattenDepth<T, U extends number = 1, Count extends unknown[] = []> = Count['length'] extends U
? T
: T extends [infer Head, ...infer Rest]
? Head extends unknown[]
? [...FlattenDepth<Head, U, [...Count, any]>, ...FlattenDepth<Rest, U, Count>]
: [Head, ...FlattenDepth<Rest, U, Count>]
: T
BEM style string
CSSの命名規則の一つであるBEMを実装してみようというやつ。
Block, Element, Modifierが与えられたときに
Block__Element--Modifier
という形式にする。Blockは文字列でElementとModifierは文字列の配列。
例えば以下のような感じ
type testCase1 = Equal<BEM<'btn', ['price'], ['warning', 'success']>, 'btn__price--warning' | 'btn__price--success' >
解答
配列からユニオンを作成するのはT[number]
でいけることを覚えていたのでそれを使う方針にした。
あとはElementとModifierが空の配列の場合があるのでその例外処理だけ気をつけて実装した。
type BEM<B extends string, E extends string[], M extends string[]> = E extends []
? `${B}--${M[number]}`
: M extends []
? `${B}__${E[number]}`
: `${B}__${E[number]}--${M[number]}`
InorderTraversal
2分木をinorder順(最も左の子が最優先でleft → parent → rightの順で見ていくやつ)で見ていってlistを作成
解答
type InorderTraversal<T extends TreeNode | null> = T extends TreeNode
? [...InorderTraversal<T['left']>, T['val'], ...InorderTraversal<T['right']>]
: []
こんなシンプルなのを作ったが再帰が深すぎる&Unionが複雑すぎると怒られた。(ただしテストケースは通っている)
理由がよく分からず解説見たらUnion distributionが発生してしまっているからっぽい。
T extends TreeNode | null
なので最初の条件判定でdistributionが発生してしまっている。
Union distributionを発生させないか、null checkをすることで解決できる。
union distributionを発生させない
type InorderTraversal<T extends TreeNode | null> = [T] extends [TreeNode]
? [...InorderTraversal<T['left']>, T['val'], ...InorderTraversal<T['right']>]
: []
null check案
type InorderTraversal<T extends TreeNode | null> =
T extends TreeNode
? [
...(
T['left'] extends TreeNode
? InorderTraversal<T['left']>
: []
),
T['val'],
...(
T['right'] extends TreeNode
? InorderTraversal<T['right']>
: []
),
]
: []
Flip
objectのkeyとpropertyを入れ替える
解答
最初シンプルに入れ替えれば良いだけだと思った
type Flip<T extends object> = {
[k in keyof T as T[k] extends PropertyKey ? T[k] : never]: k
}
上記コードを書いたところ↓のテストケースで通らなかった。
Expect<Equal<{ 3.14: 'pi'; true: 'bool' }, Flip<{ pi: 3.14; bool: true }>>>
trueがPropertyKey(string | number | symbol)ではないのでうまくいっていなかった。
ダメもとでtrueを文字列で"true"として返したらうまくいった。
最終的な解答は↓
type Flip<T extends object> = {
[k in keyof T as T[k] extends PropertyKey
? T[k]
: T[k] extends true
? "true"
: T[k] extends false
? "false"
: never
]: k
}
他の人の解答
もっと簡単にできたみたい。
type Flip<T extends Record<string, any>> = {
[Key in keyof T as `${T[Key]}`]: Key
}
こんな感じ
Fibonacci Sequence
フィボナッチ数列を作成
解答
非常に面倒だが、足し算とマイナス1を計算する型を作成してゴリ押ししたらいけた。
type NumberToArray<T extends number, Arr extends any[] = []> = Arr['length'] extends T ? Arr : NumberToArray<T, [...Arr, any]>
type Plus<T extends number, U extends number> = [...NumberToArray<T>, ...NumberToArray<U>]['length']
type MinusOne<T extends number, A extends any[] = []> = [...A, any] extends { length: T} ? A['length'] : MinusOne<T, [...A, any]>
type Fibonacci<T extends number, Current extends number = 1, Before extends number = 0> = T extends 1
? Current
: Plus<Current, Before> extends number
? Fibonacci<MinusOne<T>, Plus<Current, Before>, Current>
: never
他の人の解答
わざわざ定義しなくても現在の再帰の数、現在の数、一つ前の数の3つを引数にして再帰すればいける
こんな感じ
type Fibonacci<
T extends number,
Rec extends any[] = [any],
Current extends any[] = [any],
Prev extends any[] = []
> = Rec['length'] extends T
? Current['length']
: Fibonacci<T, [...Rec, any], [...Current, ...Prev], Current>
AllCombinations
文字列を受け取って、その文字列を構成する文字の組み合わせを全て返すというやつ
解答
難しかった。Union distributionをうまく使うんだろうなと思い、色々触ってみていたがパッとうまい方法が見つからなかった。
良い方針が見つけられず、自力正解できなかった。
方針
ヒントをもらい次の方針でいけばできそうだった。
- 文字列から各文字のUnion型に変換
- ユニオンから1文字とって、その文字自身を返す場合、その文字を使用してPermutateする場合、その文字を使用しないでAllConbinationsを再帰的に実行する場合に分ける
これを実装してみたのが以下
type StringToUnion<S extends string> = S extends `${infer Head}${infer Rest}`
? Head | StringToUnion<Rest>
: never
type Permutation<T extends string, U extends string = T> = T extends T
? [U] extends [T]
? `${T}`
: `${T}${Permutation<Exclude<U, T>>}`
: never
type IsNever<T> = [T] extends [never] ? true : false
type AllCombinations<S extends string, T extends string = StringToUnion<S>, U extends string = T> = IsNever<T> extends true
? ""
: T extends T
? [U] extends [T]
? S | ""
: S | Permutation<U> | AllCombinations<Exclude<U, T>>
: never
コードがだいぶ長くなってしまったが、これで通った。
""
がユニオンに含まれないようにするのがポイント。
他の人の解答を見たらもっと簡潔なものもあった。
Greater Than
2つのNumberを受け取って、第一引数が第二引数よりも大きいかったらtrue, それ以外はfalseを返す
解答
数字を配列に変換して、配列から要素を1つずつ取り出していき、先に第2引数の数字の配列が空になったらtrueを返すようにした。
type NumberToArray<T extends number, Arr extends any[] = []> = Arr['length'] extends T ? Arr : NumberToArray<T, [...Arr, any]>
type GreaterThan<T extends number, U extends number, TA extends any[] = NumberToArray<T>, UA extends any[] = NumberToArray<U>> = TA extends []
? false
: UA extends []
? true
: TA extends [infer TAHead, ...infer TARest]
? UA extends [infer UAHead, ...infer UARest]
? GreaterThan<T, U, TARest, UARest>
: never
: never
ただ、これだと数値が大きい時に再起が深すぎてエラーになってしまった。
他の人は数字を文字列として扱うことで再帰が深くならないようにしていた。ただ条件分けが煩雑で面倒なのでこの問題は諦めた。
追記
そこまで複雑ではない方針でいけそうだった
- 桁数を確認して多かったらgreater
- 桁数同じだったら先頭の数字を比較して大きかったらgreater
- 数字が同じだったら次の桁で2を繰り返す
Zip
pythonのzip関数と同じやつ
type exp = Zip<[1, 2], [true, false]> // expected to be [[1, true], [2, false]]
解答
両方の配列から先頭の要素を取り出して行って、取り出せなくなったらループ終了
type Zip<T extends any[], U extends any[]> = T extends [infer THead, ...infer TRest]
? U extends [infer UHead, ...infer URest]
? [[THead, UHead], ...Zip<TRest, URest>]
: []
: []
シンプルな方針でいけた。
IsTuple
Tupleを判定
解答
Tupleの定義がよく分からなかったので、とりあえずテストケースが通るような条件分岐を書いた
type IsNever<T> = [T] extends [never] ? true : false
type IsTuple<T> = IsNever<T> extends true
? false
: T extends [] | [any] | readonly [any]
? true
: false
これで通った。がこれで良いのか??(ダメそう)
他の人の解答
色々あったが↓が多そうだった。
type IsTuple<T> = [T] extends [never]
? false
: T extends readonly unknown[]
? number extends T['length']
? false
: true
: false
これは↓の性質を活かしたものらしい。
type Length<T> = T extends readonly unknown[] ? T['length'] : never
type hoge = Length<number[]> // number
Chunk
↓を満たす関数
type exp1 = Chunk<[1, 2, 3], 2> // expected to be [[1, 2], [3]]
type exp2 = Chunk<[1, 2, 3], 4> // expected to be [[1, 2, 3]]
type exp3 = Chunk<[1, 2, 3], 1> // expected to be [[1], [2], [3]]
解答
再帰の回数を3つ目の引数で計測して、その回数になったら次の再帰に移るという方法でいけた。
再帰の数よりも配列の数の方が短い場合に、途中までの配列を返さなければいけないことに注意。
type Chunk<T extends any[], U extends number, V extends any[] = []> = V['length'] extends U
? [V, ...Chunk<T, U, []>]
: T extends [infer Head, ...infer Rest]
? Chunk<Rest, U, [...V, Head]>
: V extends []
? []
: [V]
Fill
Expect<Equal<Fill<[1, 2, 3], true>, [true, true, true]>>,
Expect<Equal<Fill<[1, 2, 3], true, 0, 1>, [true, 2, 3]>>,
Expect<Equal<Fill<[1, 2, 3], true, 1, 3>, [1, true, true]>>,
Expect<Equal<Fill<[1, 2, 3], true, 10, 0>, [1, 2, 3]>>,
Expect<Equal<Fill<[1, 2, 3], true, 10, 20>, [1, 2, 3]>>,
Expect<Equal<Fill<[1, 2, 3], true, 0, 10>, [true, true, true]>>,
こんな感じでArrray, value, start, endを受け取ってstart <= index < endまでのindexの要素をvalueに置き換える。
解答
start <= index < endをうまく判定してやれば良い。
ちょっと前にGreater thanをやったのでそれが使えそうだと思った。
第5引数に変換後の値を詰めるarrayを作成して、そのarrayの長さで今のindexを判定することにした。
ちょっと長くなってしまったが、↓の感じで行けた。
type GreatherThan = // GreatherThan参照
type GreaterEqual<T extends number, U extends number> = T extends U
? true
: GreaterThan<T, U> extends true
? true
: false
type Fill<
T extends unknown[],
N,
Start extends number = 0,
End extends number = T['length'],
A extends unknown[] = [],
> = T extends [infer Head, ...infer Rest]
? Fill<Rest, N, Start, End, [...A, GreaterEqual<A['length'], Start> extends true
? GreaterThan<End, A['length']> extends true
? N
: Head
: Head
]>
: A
Without
第一引数の配列から第二引数で指定された配列または値を除外する
解答
配列の中に値が含まれているかどうかを判定するIncludeを定義し、それを利用し配列の要素を一つずつ見ていくことで除外していった。
type Include<Target extends any[], Value extends any> = Target extends [infer Head, ...infer Rest]
? Equal<Head, Value> extends true
? true
: Include<Rest, Value>
: false
type Without<T extends any[], U extends any[] | any, Result extends any[] = []> = T extends [infer Head, ...infer Rest]
? U extends any[]
? Include<U, Head> extends true
? Without<Rest, U, Result>
: Without<Rest, U, [...Result, Head]>
: Equal<Head, U> extends true
? Without<Rest, U, Result>
: Without<Rest, U, [...Result, Head]>
: Result
Trunc
数字または文字から整数部分のみを取り出す
解答
文字列として扱えば良い
type Trunc<T extends number | string> = `${T}` extends `${infer Int}.${any}`
? Int extends ''
? '0'
: Int
: `${T}`
IndexOf
ArrayとValueが与えられらときに、Valueと一致するIndexを取得(一致する先頭のIndex)
解答
現在のIndexを保持する配列を第3引数に持っておく。
先頭から要素を見ていって一致したらそのIndexを返す
type IndexOf<T extends any[], U extends unknown, Checked extends any[] = []> = T extends [infer Head, ...infer Rest]
? Equal<Head, U> extends true
? Checked['length']
: IndexOf<Rest, U, [...Checked, any]>
: -1
Join
配列T、文字列または数値Uを受け取り、Tの各要素の間にUを入れる
解答
配列の要素を一つずつ見てその間に入れていく。配列の各要素の間にだけ入れることに注意。
type Join<T extends unknown[], U extends string | number, Ret extends string = ''> = T extends [infer Head, ...infer Rest]
? Head extends string
? Join<Rest, U, Ret extends '' ? Head : `${Ret}${U}${Head}`>
: never
: Ret
LastIndexOf
IndexOfの先頭のIndexではなく、最後のIndexを返す版
解答
先頭から見ていって一致したらIndexを更新していく
type LastIndexOf<T extends any[], U extends unknown, Checked extends any[] = [], Index extends number = -1> = T extends [infer Head, ...infer Rest]
? Equal<Head, U> extends true
? LastIndexOf<Rest, U, [...Checked, any], Checked['length']>
: LastIndexOf<Rest, U, [...Checked, any], Index>
: Index
先頭から見ないでTailから見る方法もある? Indexの取得がtailからだと厳しそう
Unique
配列からユニークな要素を抜き出す
解答
配列の中に要素が含まれているかを判定するIncludeを定義して、含まれていたらスルー、含まれていなかったら追加を繰り返す。
type Include<T extends any[], Value extends any> = T extends [infer Head, ...infer Rest]
? Equal<Head, Value> extends true
? true
: Include<Rest, Value>
: false
type Unique<T extends any[], U extends any[] = []> = T extends [infer Head, ...infer Rest]
? Unique<Rest, Include<U, Head> extends true ? U : [...U, Head]>
: U
MapType
type StringToNumber = { mapFrom: string; mapTo: number;}
MapTypes<{iWillBeANumberOneDay: string}, StringToNumber> // gives { iWillBeANumberOneDay: number; }
こんな感じでMapを受け取って、そのMapに従って型を変える
type StringToNumber = { mapFrom: string; mapTo: number;}
type StringToDate = { mapFrom: string; mapTo: Date;}
MapTypes<{iWillBeNumberOrDate: string}, StringToDate | StringToNumber> // gives { iWillBeNumberOrDate: number | Date; }
Mapがユニオンである場合もあるので注意
解答
ユニオンがない場合はMappedTypeで超単純に実装すれば良いだけなので簡単。
だが、ユニオンの場合が難しかった。ユニオンが与えられた際にはdistributionを発生させなければいけない。MappedTypeでも自動的にdistributionが発生する場合がある([k in keyof T]
のTがユニオンの場合)が今回はそれに該当しない。
R extends R
でdistributionを発生することができるが、Rがユニオンだったかどうかで分岐を発生させなければいけない。ユニオンを判定するためにRを代入したS
を第3引数に置いた。
distributionを発生させた後にSがRをextendしていれば(S extends R
)ユニオンではないと判断できると考えた。
しかしながらS extends R
ではユニオンが判断できずテストケースが通らなかった。
試行錯誤し、S[mapFrom'] extends R['mapFrom']
とすることでユニオンと判断できることがわかった。(なぜ要素までアクセスすると良いのかはよく分かっていない)
type Map = {
mapFrom: any;
mapTo: any;
}
type MapTypes<T extends object, R extends Map, S extends Map = R> = {
[k in keyof T]: R extends R
? Equal<T[k], R['mapFrom']> extends true
? R['mapTo']
: S['mapFrom'] extends R['mapFrom']
? T[k]
: never
: never
}
ConstructTuple
numberを受け取って、そのnumberの長さのunknownの配列を作成
解答
特にコメントすることなし
type ConstructTuple<L extends number, Ret extends unknown[] = []> = Ret['length'] extends L
? Ret
: ConstructTuple<L, [...Ret, unknown]>
Number Range
type result = NumberRange<2 , 9> // | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
↑のようなRangeを生成
解答
type NumberRange<L, H>
のとき、[L, L+1, ..., H -1, H] の配列Aを作成して、A[number]でいけそう
↓
type PlusOne<T extends number, A extends any[] = []> = A['length'] extends T ? [...A, any]['length'] : PlusOne<T, [...A, any]>
type NumberRange<L extends number, H extends number, Current extends number = L, Arr extends number[] = []> = Current extends H
? [...Arr, Current][number]
: NumberRange<L, H, PlusOne<Current>, [...Arr, Current]>
Combination
文字列の配列を受け取って、それらの全ての組み合わせを得る
解答
文字列の配列Tを受け取り、T[number]でユニオンに変換した後、Union distributionを使えば良い。
type Combination<T extends string[], U extends string = T[number], V extends string= U> =
U extends U
? U | `${U} ${Combination<T, Exclude<V, U>>}`
: never
Subsequence
配列の部分配列を取得
// 例
Equal<Subsequence<[1, 2, 3]>, [] | [1] | [2] | [1, 2] | [3] | [1, 3] | [2, 3] | [1, 2, 3] >
解答
先頭から要素を一つずつ見ていって、その要素を使うか使わないかを判断していけば良い
type Subsequence<T extends any[], Ret extends any[] = []> = T extends [infer Head, ...infer Rest]
? Subsequence<Rest, [...Ret, Head]> | Subsequence<Rest, Ret>
: Ret
CheckRepeatedChars
文字列中に同じ文字が2回以上含まれているかを判定
解答
配列に文字を入れていって、その配列中に既に文字が含まれていたらtrueを返す
type CheckRepeatedChars<T extends string, U extends string[] = []> = T extends `${infer Head}${infer Rest}`
? Includes<U, Head> extends true
? true
: CheckRepeatedChars<Rest, [...U, Head]>
: false
FirstUniqueCharIndex
文字列を先頭から見ていって最初のUniqueな文字のIndexを返す
解答
文字列の中で、ある文字がUniqueかを返すCheckUniqueを定義して、文字列を最初から見ていった時にCheckUniqueがtrueの文字のIndexを返すという方針でできた
type CheckUnique<Str extends string, Char extends string, Appeared extends boolean = false> = Str extends `${infer Head}${infer Rest}`
? Head extends Char
? Appeared extends true
? false
: CheckUnique<Rest, Char, true>
: CheckUnique<Rest, Char, Appeared>
: true
type FirstUniqueCharIndex<T extends string, U extends string = T, IndexList extends string[] = []> = T extends `${infer Head}${infer Rest}`
? CheckUnique<U, Head> extends true
? IndexList['length']
: FirstUniqueCharIndex<Rest, U, [...IndexList, Head]>
: -1
GetMiddleElement
配列を受け取って真ん中の要素を返す(配列長が偶数の場合は2つ、奇数の場合は1つを返す)
解答
配列を先頭、末尾、それ以外に分解して分解できなくなったら配列を返す
type GetMiddleElement<T extends any[]> = T extends [infer Head, ...infer Rest, infer Tail]
? Rest extends []
? T
: GetMiddleElement<Rest>
: T
あまり見なかった面白い問題
Appear only once
配列の中からUniqueな要素のみを抜き出す
解答
配列の中でUniqueかを判定するCheckUniqueを定義し、配列の要素を先頭から見ていった時にUniqueだったらその要素を配列に加え、Uniqueでなかったら配列に加えずそのままという操作を繰り返す
type CheckUnique<T extends any[], U extends unknown, Appeared extends boolean = false> = T extends [infer Head, ...infer Rest]
? Equal<Head, U> extends true
? Appeared extends true
? false
: CheckUnique<Rest, U, true>
: CheckUnique<Rest, U, Appeared>
: true
type FindEles<T extends any[], U extends any[] = T, Ret extends any[] = []> = U extends [infer Head, ...infer Rest]
? CheckUnique<T, Head> extends true
? FindEles<T, Rest, [...Ret, Head]>
: FindEles<T, Rest, Ret>
: Ret
Integer
整数かどうかを判定する
解答
整数部と少数部に分解して、少数部が0のみだったら整数、0以外が含まれていたら少数と判定した。
また、number型を除外するためにEqual<T, number> extends true
を用いた。
type ZeroOnly<T extends string> = T extends `${infer Head}${infer Rest}`
? Head extends '0'
? ZeroOnly<Rest>
: false
: true
type Integer<T extends number> = Equal<T, number> extends true
? never
: `${T}` extends `${any}.${infer Decimal}`
? ZeroOnly<Decimal> extends true
? T
: never
: T
Primitive
objectのpropertyを全てprimitiveな型に変換
解答
愚直に各primitiveかどうかを判定してそれを返すようにした。
listの場合は再帰処理が必要なのでprimitive化する型を作成した。
また、objectの場合も再帰処理が必要。
関数がobjectとして判定されてしまうので、関数かどうかはobjectかどうかを判定する前にも必要。
type PrimitiviseList<T extends any[]> = T extends [infer Head, ...infer Rest]
? [Primitivise<Head>, ...PrimitiviseList<Rest>]
: []
type Primitivise<T> = T extends any[]
? PrimitiviseList<T>
: T extends string
? string
: T extends number
? number
: T extends boolean
? boolean
: T extends (...args: any) => void
? Function
: never
type ToPrimitive<T extends object> = {
[k in keyof T]: T[k] extends (...args: any) => void
? Function
: T[k] extends object
? ToPrimitive<T[k]>
: Primitivise<T[k]>
}
無駄に長くなってしまっている。
他の人の解答
他の人の解答見たらもっとスマートなものがあった。
type ToPrimitive<T> = T extends Function
? Function
: T extends object
? {
[K in keyof T]: ToPrimitive<T[K]>
}
: T extends boolean
? boolean
: T extends string
? string
: T extends number
? number
: never;
一番外側でmapped typesを使わずに、objectだった場合にmapped typesを使うようにしている。
こうすることでわざわざ他の型を定義せずに済む。(リストがどうやって処理されているかはいまいちわかっていない)
DeepMutable
全てmutableに変換
解答
- readonly
でreadonlyを外せるのでそれを使う。
type DeepMutable<T extends object> = {
- readonly [k in keyof T]: T[k] extends Function
? T[k]
: T[k] extends object
? DeepMutable<T[k]>
: T[k]
}
Functionがobjectとして判定されるのに気をつける
All
第一引数で与えられる配列の全ての要素が第二引数と等しいかを判定
解答
先頭から全ての要素が等しいかどうかを判定していく
type All<T extends any[], U> = T extends [infer Head, ...infer Rest]
? Equal<Head, U> extends true
? All<Rest, U>
: false
: true
Filter
第一引数で与えられる配列の中から、第二引数で与えられるプリミティブな値を継承しているもののみの配列をフィルタリングして返す
解答
シンプルに先頭から要素を見ていって継承しているかを判定する
type Filter<T extends any[], P, Ret extends any[]= []> = T extends [infer Head, ...infer Rest]
? Head extends P
? Filter<Rest, P, [...Ret, Head]>
: Filter<Rest, P, Ret>
: Ret
Combination key type
配列の中からペアの組み合わせを求める
ペアを作る際、配列の順序が早い方が必ず先に来るようにする
解答
配列を先頭から取り出していき、先頭を使う場合使わない場合に分け、使う場合は保存しておく。
既に保存されたものがある場合はそれを使用して文字列を作る。
これを繰り返すことでできた。
type ModifierKeys = ['cmd', 'ctrl', 'opt', 'fn']
type _Combs<T extends unknown[], First extends string = ''> = T extends [infer Head, ...infer Rest]
? First extends ''
? Head extends string
? _Combs<Rest, `${Head}`> | _Combs<Rest, ''>
: never
: Head extends string
? `${First} ${Head}` | _Combs<Rest, First>
: never
: never
type Combs = _Combs<ModifierKeys>
他の人の解答
もっとスマートなやり方があった。
type ModifierKeys = ['cmd', 'ctrl', 'opt', 'fn']
type Combination<T extends string[]> = T extends [infer F extends string, ...infer R extends string[]]
? `${F} ${R[number]}` | Combination<R>
: never
type Combs = Combination<ModifierKeys>
こんな感じで残りの要素をRest[number]でdistributionしながら指定している。すごい。
また、配列から要素を取り出す際にも[infer Head, ...infer Rest]
だと型がunknownになってしまうが
[infer Head extends string, ...infer Rest extends string[]]
とすることでstring型になるようにしている。このやり方初めて知った!!
勉強になった。
ReplaceFirst
タプルTに最初に現れるSをRに置き換えるReplaceFirst<T, S, R>型を実装
解答
1回変換したらS, Rにnullを入れる
type ReplaceFirst<T extends readonly unknown[], S, R, Ret extends unknown[] = []> = T extends [infer Head, ...infer Rest]
? Head extends S
? ReplaceFirst<Rest, null, null, [...Ret, R]>
: ReplaceFirst<Rest, S, R, [...Ret, Head]>
: Ret