Type Challenges ログ
過去ログ 2025/04/30
slackに色々書いてたメモを転記する。
昨日はこれやってた
type Includes<T extends readonly any[], U> = Extract<T[number], U> extends T ? false : true;
これで行けるかと思ったが、思ったより複雑な回答だった
解答例にあったこれはNG
type Includes<T extends readonly any[], U> = {
[P in T[number]]: true
}[U] extends true ? true : false;
次のケースがクリアできない。Tの要素をオブジェクトのキーに当てているので、string, number, symbol 以外の方に対応できない
Expect<Equal<Includes<[false, 2, 3, 5, 6, 7], false>, true>>
Expect<Equal<Includes<[1 | 2], 1>, false>>
そして同じところに書いてあったこれは……
type IsEqual<T, U> =
(<G>() => G extends T ? 1 : 2) extends
(<G>() => G extends U ? 1 : 2)
? true
: false;
type Includes<Value extends any[], Item> =
IsEqual<Value[0], Item> extends true
? true
: Value extends [Value[0], ...infer rest]
? Includes<rest, Item>
: false;
IsEqual
が理解できてない。
<G>() => G
が T の拡張かどうか と <G>() => G
が U の拡張かどうかで判断している。 <G>() => G extends Hoge ? 1 : 2
の Hoge に当てはまる型が一致していれば extends の条件で true になるというだけだ。
で、tuple の要素ごとに一致判定をしていくと。
このケースがクリアできない
Expect<Equal<Includes<[null], undefined>, false>>,
最終的にこれが答えになるっぽい
type IsEqual<T, U> =
(<G>() => G extends T ? 1 : 2) extends
(<G>() => G extends U ? 1 : 2)
? true
: false;
type Includes<T extends readonly any[], U> = {
[K in keyof T]: IsEqual<T[K], U>;
} extends false[] ? false : true;
keyof T
はどうなるか。
type IncludesTest<T extends readonly any[], U> = {
[K in keyof T]: IsEqual<T[K], U>;
}
これは boolean[]
の形式になる。実際はより狭い型になるが
type Test<T extends readonly any[]> = {[K in keyof T]: K};
type Test1 = Test<['Kars', 'Esidisi', 'Wamuu', 'Santana']>
この場合、 type Test1 = ["0", "1", "2", "3"]
となる。
つまりタプルはsrting integerをキーに持つオブジェクトということ?
解答のアイデア
-
タプルTの要素となる型と型Uが一致する ことを IsEqual で判定する
- そのまま extends を使っても一致判定はできない
type IsEqual<T, U> = T extends U ? U extends T ? true : false : false
- そのまま extends を使っても一致判定はできない
- タプルTの全要素の型について IsEqual で判定を行う
- 再帰を使うと
[]
の要素とundefined
が一致してしまうため、Includes<[null], undefined>
の判定にこける - タプルから新しく
boolean[]
な型を生成してfalse[]
との一致を比較する
- 再帰を使うと
過去ログ 5/6
1週間ぶり
前回はIncludesだったはずなので、Pushから
めちゃしんぷるだった
type Push<T, U> = T extends readonly any[] ? [...T, U] : never;
Unshift も簡単だったらいいな
めっちゃ簡単だった
type Unshift<T extends readonly any[], U> = [U, ...T]
Pushの時は型定義のタイミングで extends で条件分岐したけど、型変数の段階で絞り込んだ方が親切だな。
type Push<T extends readonly any[], U> = [...T, U]
Parametersはちょっとkeiroが違う
infer で推論してそれを取り出す
type MyParameters<T extends (...args: any[]) => any> = T extends (...args: infer U) => any ? U : never
組み込みで Parameters っていう型があるんだな。
初級編終わった。Includesだけ妙に複雑だったな。
過去ログ 2025/05/08
Deep Readonly
これは再帰的な型定義が必要になるやつかな
shallowなReadonlyはこう
type DeepReadonly<T> = {readonly [key in keyof T]: T[key]};
これだとオブジェクト型の要素内まで掘ってreadonlyをつけることができない
そこで再帰的に書いてみる
type DeepReadonly<T> = {readonly [key in keyof T]: DeepReadonly<T[key]>};
要素がPrimitiveなケースが抜けているので転ける
そこで extends {}
で条件付けをしてみる。
type DeepReadonly<T> = {readonly [Key in keyof T]: T[Key] extends {} ? DeepReadonly<T[Key]> : T[Key]};
が、これでも関数が{}の拡張になってしまうのでダメ。というか結果が変わってない。
すぐわかんないから答え見てみよう
一番上に出てきたやつ
type DeepReadonly<T> = T extends never ? T : { readonly [P in keyof T]: DeepReadonly<T[P]> }
これだと関数が空のオブジェクトになってしまう。
issueの解答例が参考になるな。
基本的なアイデアは
- Primitive な型を定義する
- value が Primitive な型であればそのまま通す、そうでなければ再帰的にvalueを評価する
解答例で割とシンプルだったもの
type Primitive = string | number | boolean | symbol | Function
type DeepReadonly<T> = T extends Primitive
? T
: { readonly [Key in keyof T]: DeepReadonly<T[Key]> };
さらにシンプルにするとこうなる。
type DeepReadonly<T> = T extends never | Function
? T
: { readonly [Key in keyof T]: DeepReadonly<T[Key]> };
ArrayLike な場合にも対応すると、、、
いやこれでうまくいってるな。
TypeScriptの型入門 #TypeScript - Qiita
ちょっと古い資料だけどよい。
Qiitaの記事内で昔のPRから引用しているやつ。こうやって定義しているらしい。
NonFunctionPropertyNames<T>
を使うことで、元の型からメソッドを除いている。
type DeepReadonly<T> =
T extends any[] ? DeepReadonlyArray<T[number]> :
T extends object ? DeepReadonlyObject<T> :
T;
interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}
type DeepReadonlyObject<T> = {
readonly [P in NonFunctionPropertyNames<T>]: DeepReadonly<T[P]>;
};
type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T];
過去ログ 2025/05/07
Get Return Type
infer の例題っぽい。
type MyReturnType<T> = T extends (...args: any[]) => (infer U) ? U : never;
TSの実装はこれかしら。
確かに関数の引数は any[] で絞らなくて良いな。
Omit
この辺までは前にやった覚えがあるな。
わかんないから答え見てしまった
うまくいく例
type MyOmit<T, K extends keyof T> = { [P in keyof T as P extends K ? never : P]: T[P] }
うまくいかない例
# Tのパラメータに以外の値がKに含まれていてもエラーにならない
type MyOmit<T, K> = { [P in keyof T as P extends K ? never : P]: T[P] }
# 要素が空になる
type MyOmit<T, K> = { [P in keyof T extends K ? never : keyof T]: T[P] }
Readonly 2
これは intersection で繋げば良い的な話?
最終的にこうなった
type MyReadonly2<T, K extends keyof T = keyof T>
= { [P in keyof T as P extends K ? never : P]: T[P] }
& { readonly [P in keyof T as P extends K ? P : never]: T[P] };
長いので一つずつ見ていく
-
MyReadonly2<T, K extends keyof T = keyof T>
- K に extends で制約をつけるのと、利用時に指定されてない場合を考慮して初期値(型)を指定する
-
{ [P in keyof T as P extends K ? never : P]: T[P] }
- Omit と同じテクニック。
as
で key remapping するのが重要
- Omit と同じテクニック。
-
{ readonly [P in keyof T as P extends K ? P : never]: T[P]}
- これも Omit と同じ。
そして、{ [ P in ... ]: T[P] }
はそれぞれTSのユーティリティ型であるPickとOmitが使える。つまりこう。
type MyReadonly2<T, K extends keyof T = keyof T> = Omit<T, K> & Readonly<Pick<T, K>>
過去ログ 2025/05/09
Tuple to Union
number
使えば ok のはず
type TupleToUnion<T extends readonly any[]> = T[number]
解説記事があった。斜め読みだけした。
実は前の問題でタプルについて推論していた。(当然忘れている
つまりタプルはsrting integerをキーに持つオブジェクトということ?
過去ログ 2025/05/12
Chainable Options
これはなんかややこしそうだな。
雑に考えて書いてみた。
type Chainable<T = unknown> = {
option<U>(key: string, value: U): Chainable<T & {[key: string]: U}>;
get(): T
}
キーの指定ができず。
optionsの引数 key
の値を形に取り込む方法がわかんね。
試行錯誤した結果、まずこうなった
type Chainable<T = unknown> = {
option<K extends string, V>(key: K, value: V): Chainable<T & {[key in K]: V}>;
get(): T
}
特定のキーが含まれているかどうかで判定したい。
これはNGなケース
type Chainable<T = unknown> = {
option<K extends string, V>(key: K, value: V): {[key in K]: V} extends T ? never : Chainable<T & {[key in K]: V}>;
get(): T
}
うまく判定できてない
Tのキーの中に特定のキーが含まれているかどうかを判定できれば良い。つまりこう。
type Chainable<T = unknown> = {
option<K extends string, V>(key: K, value: V): K extends keyof T ? never : Chainable<T & {[key in K]: V}>;
get(): T
}
が、だめ。今度はTSエラーを突破してしまう。
tsとしてエラーになるためにはどうなってればええんや
回答みよ
一番上に書いてあったやつ
type Chainable<T = {}> = {
option: <K extends string, V>(key: K extends keyof T ?
V extends T[K] ? never : K
: K, value: V) => Chainable<Omit<T, K> & Record<K, V>>
get: () => T
}
minor fix のこれで通過した
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;
};
ちょっと惜しい
自分の回答をベースに書き直すとこうなった
type Chainable<T = unknown> = {
option<K extends string, V>(key: K extends keyof T ? never : K, value: V):
Chainable<Omit<T, K> & {[key in K]: V}>;
get(): T
}
おわり。
MyOmitやったよなと思って回答みたら結構ややこしいやつだった。やっぱ記憶は揮発するな。脳は揮発性メモリ。
ポイント
- 返り値の型定義で condition を書くと never が chain されるため
get()
の呼び出してエラーになる。ので、引数 key の型定義側でconditionを埋め込むことで options() の type error に持ち込む。 - テストケースを踏まえると、 optionsの返り値の要素の型はあと勝ちになるので、Omitを使う。
- が、そもそもキーの重複を許容しないという条件があるのでここは本質的ではない。
過去ログ 2025/05/13
15 Last of Array
type-challenges/questions/00015-medium-last/README.ja.md at main · type-challenges/type-challenges
前にやったFirst of Array はこんな感じだったはず
type First<T extends any[]> = T extends [infer F, ...infer R] ? F : never;
同じ要領でスプレッド構文が使えたらいいな。
つかえた。本当にこれでいいのか不安になるな。
type Last<T extends any[]> = T extends [...infer R, infer L] ? L : never;
あと、タプル型に対して負のインデックスを与えるとUnion Typeに変換できた。数に応じてunionの際の順序が入れ替わる様子。
type Test<T extends any[]> = T[-1];
type T1 = Last<[3, 2, 1]>
↑の場合、T2は次の型になる。末尾の 1
が Union Type の先頭にくる。
type T1 = 1 | 3 | 2
ついでにunion typeからtupleへの変換が難しそうという記事を見た。
16 Pop
配列の末尾を取り除く。15 Last of Array と同じ解き方。
type Pop<T extends any[]> = T extends [...infer R, infer L] ? R : []
解答例を見た。
16 - Pop · Issue #37 · type-challenges/type-challenges
これが良さげ。
type Pop<T extends any[]> = T extends [...infer Rest, infer _] ? Rest : T
20 Promise.all
Promise.all の主に返り値の型を考える問題。
色々試行錯誤して、まずはこうなった。
declare function PromiseAll<T>(values: T): T extends Promise<infer R>[] ? R : Promise<T>
が、ダメ。distributive な動きを期待したが、うまく動かず。PromiseLikeな型を抽出できない。
const promiseAllTest1: Promise<readonly [1, 2, 3]>
const promiseAllTest2: Promise<readonly [1, 2, Promise<number>]>
const promiseAllTest3: Promise<(number | Promise<number>)[]>
const promiseAllTest4: Promise<(number | Promise<number>)[]>
const promiseAllTest5: Promise<(number | Promise<string>)[]>
前にやった問題があった。
189 - Awaited · Issue #24969 · type-challenges/type-challenges
- PromiseLike<T> という組み込み型が使えそう。
- あと、タプル型であれば rest elements を取り出して色々できそう
というわけでこうしてみた。
type MyAwaitedAll<T extends unknown[]> = T extends [infer F, ...infer R]
? F extends PromiseLike<infer P>
? [P, ...MyAwaitedAll<R>]
: [F, ...MyAwaitedAll<R>]
: []
declare function PromiseAll<T extends any[]>(values: T): Promise<MyAwaitedAll<T>>
が、配列型の評価時に T extends [infer F, ...infer R]
が通らない。配列型はタプル型ではないから通らないのか。
答えを見る。
20 - Promise.all · Issue #211 · type-challenges/type-challenges
declare function PromiseAll<T extends any[]>(values: readonly [...T]): Promise<{
[K in keyof T]: Awaited<T[K]>
}>
めっちゃシンプル。Mapped Type を使えばタプル型も配列型もうまく表現できるのか。
あとは組み込みのAwatiedも使えた。
62. Type Lookup
この問題の設定に限るのであればこれで良いよな。でも流石に違うはず。
type LookUp<U extends { type: string }, T> = T extends 'dog' ? Dog : T extends 'cat' ? Cat : never
ちょっとわからなかったので答えを見た。これはいいね。
type LookUp<U, T> = U extends { type: T } ? U : never
106. Trim Left
文字列を受け取り、先頭の空白を削除した新しい文字列を返す
TrimLeft<T>
を実装します。
型を使って文字列を操作するってどうすりゃええんや。
一旦この辺り読んで考えてみよう。
TypeScript: Documentation - Template Literal Types
決まった文字列を足すだけなら簡単だよな。
type ConcatSpaceLeft<T extends string> = ` ${T}`;
テンプレートリテラルは直接は使えないかと思った。が、再帰的に使えばいけるかも。
type TrimLeft<S extends string> = S extends ` ${infer R}` ? TrimLeft<R> : S;
スペース
だけだとここでこける。
type T5 = TrimLeft<' \n\t foo bar '>;
type T7 = TrimLeft<' \n\t'>
場合分けとかできるかな。いけた。Union Typeに対応していた。
type TrimLeft<S extends string> = S extends `${' ' | '\t' | '\n'}${infer R}` ? TrimLeft<R> : S;
正規表現の \S
では
, \t
, \n
, \r
, \f
を拾うので、こうなる。
type TrimLeft<S extends string> =
S extends `${' ' | '\t' | '\n' | '\r' | '\f'}${infer R}`
? TrimLeft<R>
: S;
解答例を見たら Space
という型に切り出していた。確かにね。
106 - TrimLeft<T> · Issue #346 · type-challenges/type-challenges
type Space = ' ' | '\n' | '\t'
type TrimLeft<S extends string> = S extends `${Space}${infer R}` ? TrimLeft<R> : S
108. Trim
Trim Left とほぼおなじような。
こう。
type Space = ' ' | '\n' | '\t' | '\r' | '\f';
type Trim<S extends string> =
S extends `${Space}${infer R}`
? Trim<R>
: S extends `${infer R}${Space}`
? Trim<R>
: S
110. Capitalize
文字列の最初の文字を大文字に変換し、それ以外はそのままにする Capitalize<T> を実装します。
なんだと。
すでに組み込み型がある。
そしてTS4.1の実装はJSの文字列処理関数でやってるっぽい
function applyStringMapping(symbol: Symbol, str: string) {
switch (intrinsicTypeKinds.get(symbol.escapedName as string)) {
case IntrinsicTypeKind.Uppercase: return str.toUpperCase();
case IntrinsicTypeKind.Lowercase: return str.toLowerCase();
case IntrinsicTypeKind.Capitalize: return str.charAt(0).toUpperCase() + str.slice(1);
case IntrinsicTypeKind.Uncapitalize: return str.charAt(0).toLowerCase() + str.slice(1);
}
return str;
}
ならば力技あるのみ。
const toCaps = {
'a': 'A', 'b': 'B', 'c': 'C', 'd': 'D', 'e': 'E',
'f': 'F', 'g': 'G', 'h': 'H', 'i': 'I', 'j': 'J',
'k': 'K', 'l': 'L', 'm': 'M', 'n': 'N', 'o': 'O',
'p': 'P', 'q': 'Q', 'r': 'R', 's': 'S', 't': 'T',
'u': 'U', 'v': 'V', 'w': 'W', 'x': 'X', 'y': 'Y',
'z': 'Z',
} as const;
type ToCaps = typeof toCaps;
type Lowercases = keyof ToCaps;
type MyCapitalize<S extends string> =
S extends `${infer F}${infer R}`
? F extends Lowercases
? `${ToCaps[F]}${R}`
: S
: S
通った。
回答を見る。
Uppercase<Stringtype>
使えるやんけ
type MyCapitalize<S extends string> =
S extends `${infer F}${infer R}`
? `${Uppercase<F>}${R}`
: S;
あとこれは勉強になる。テンプレート型でinfer使って推論する時の判定方法。
116. Replace
文字列Sに含まれる文字FromをToに一度だけ置き換える型Replace<S, From, To>を実装します。
またも文字列だ。これまでやったことを踏まえて、こう書いてみた。
type Replace<S extends string, From extends string, To extends string> =
S extends `${From}${infer R}`
? `${To}${R}`
: S extends `${infer F}${infer R}`
? `${F}${Replace<R, From, To>}`
: S
が、1つおかしなケースが。
別の値で試してみた。なるほど、先頭が空文字列と一致してしまうのか。
無理やり条件分岐増やして対応した。
type Replace<S extends string, From extends string, To extends string> =
From extends ''
? S
: S extends `${From}${infer R}`
? `${To}${R}`
: S extends `${infer F}${infer R}`
? `${F}${Replace<R, From, To>}`
: S
あんま綺麗じゃないな。解答をみよう。
type Replace<S extends string, From extends string, To extends string> =
From extends ''
? S
: S extends `${infer V}${From}${infer R}`
? `${V}${To}${R}`
: S
わざわざ再帰使わなくてもよかったやん。なるほどね。
119. ReplaceAll
Replaceの解答をちょっと変えれば良さげ。
type ReplaceAll<S extends string, From extends string, To extends string> =
From extends ''
? S
: S extends `${From}${infer R}`
? `${To}${ReplaceAll<R, From, To>}`
: S extends `${infer F}${infer R}`
? `${F}${ReplaceAll<R, From, To>}`
: S
${infer F}${infer R}
で 1文字ずつ判定していくイメージ。解答も同じ感じだったが最適化されてる。
119 - ReplaceAll · Issue #367 · type-challenges/type-challenges
type ReplaceAll<S extends string, From extends string, To extends string> =
From extends ''
? S
: S extends `${infer R1}${From}${infer R2}`
? `${R1}${To}${ReplaceAll<R2, From, To>}`
: S
${infer R1}
は ''
も含むからこれでいける。
191. Append Argument
与えられた関数型 Fn と任意の型 A に対して、第一引数に Fn を取り、第二引数に A を取り、Fn の引数に A を追加した関数型 G を生成します。
関数に特定の型の引数を追加する。
はじめは↓みたいに関数型を表現して推論しようとしていたが、これは間違った表記。
type AppendArgument<Fn extends Function, A> =
Fn extends (...infer Args) => infer Ret
? ([...Args, A]) => Ret
: never
正しくはこうだった。
type AppendArgument<Fn extends Function, A> =
Fn extends (...args: infer Args) => infer Ret
? (...args: [...Args, A]) => Ret
: never
296. Permutation
permutation: 順列、交換、置換、並べ換え (英語「permutation」の意味・使い方・読み方 | Weblio英和辞書)
Union 型を Union 型の値の順列を含む配列に変換する順列型を実装します。
Union to Tupleだ。なんか難しそうだけど、前に一回こんな形の型を作ったことがある気がする。
まずはオブジェクトとして取り出してみる。
type Permutation<T extends string> = {[Key in T]: Key}
これは単に { A: 'A'}
みたいなオブジェクト型になるだけ。
前に調べたのはこれだ。
まんま順列の実装が書いてあった。
type Permutation<T, Orig=T> =
[T] extends [never]
? []
: T extends unknown
? [T, ...Permutation<Exclude<Orig, T>>]
: never
ただ、なんでこうなるかが理解できない。解答の解説を読む。
type loopUnion<Union extends string, Item extends string = Union> = Item extends Item ? `loop ${Item}` : never;
type result = loopUnion<"A" | "B" | "C">; // "loop A" | "loop B" | "loop C"
Union Typeのcondition checkではUnion Typeのそれぞれの型を評価して再結合するのか。
TypeScript: Documentation - Conditional Types
基本的に T extends unknown
は true になり、 ?
以降はUnion Typeの各型を評価する形になる。[T, ...Permutation<Exclude<Orig, T>>]
の T
は本来のTではなく、分割された型となるので、例えば T = 'A' | 'B'
を渡した時、 ?
の右辺では [ 'A', ...Permutation<Exclude<'A' | 'B', 'A'>>]
と [ 'B', ...Permutation<Exclude<'A' | 'B', 'B'>>]
をそれぞれ評価して Union Typeとして結合する。内側のPermutationでは各型を評価してそれを再びUnion Typeに統合するため、結果的に順列のUnion Typeが構成される。
つまり、こんな感じか。
Permutaion<'A' | 'B'>
= [ 'A', ...Permutation<Exclude<'A' | 'B', 'A'>>] | [ 'B', ...Permutation<Exclude<'A' | 'B', 'B'>>]
= [ 'A', ...Permutation<'B'> ] | [ 'B', ...Permutation<'A'> ]
= [ 'A', ...['B'] ] | [ 'B', ...['A'] ]
= [ 'A', 'B' ] | [ 'B', 'A' ]
Union Type の要素が3つ以上あるときは、[ 'A', ...([ 'B', 'C' ] | [ 'C', 'B' ]) ] = [ 'A', 'B', 'C' ] | [ 'A', 'C', 'B' ]
みたいな評価が入るってことか。
実際に試してみたらその通りだった。
298. Length of String
文字列の長さを返す型か。
type StringToTuple<S extends string>
= S extends `${infer F}${infer R}`
? [F, ...(StringToTuple<R>)]
: [];
type LengthOfString<S extends string> = StringToTuple<S>['length'];
タプルに変換して長さを出す。
459. Flatten
タプル型のネストを取り除いてフラット化する
とりあえず要素を一つずつ推論して、再帰的にFlattenをかけていく。
type Flatten<T extends readonly any[]> =
T extends [infer F, ...infer R]
? F extends readonly any[]
? [...(Flatten<F>), ...(Flatten<R>)]
: [F, ...(Flatten<R>)]
: T
多分もっと短くなると思ったけど、これでも良さそうだった。
527. Append to object
Object型に要素を追加する。
intersectionで表現すればいけんじゃねと思ったけどダメだった。
type AppendToObject<T, U extends string | number | symbol, V> =
{ [K in (keyof T)]: T[K] } & { [K in U]: V };
conditionをvalue側に埋め込んだらうまく行った。
type AppendToObject<T, U extends string | number | symbol, V> =
{ [K in (keyof T) | U]: K extends keyof T ? T[K] : V };
解答例を見る。
U extends keyof any
でオブジェクトのキーになりうる型 string | number | symbol
という制約をかけている。なるほどね。
type AppendToObject<T, U extends keyof any, V> = {
[K in keyof T | U]: K extends keyof T ? T[K] : V;
};
529. Absolute
絶対値への変換となっているが、実質は数値等から文字列型への変換。
文字列のテンプレートリテラル型を使えばいけるのでは、と思った。まずは単純な文字列への変換。
type Absolute<T extends number | string | bigint> = `${T}`;
文字列に変換した時に '-'
が先頭に来たら取り除けば良い?
type Absolute<T extends number | string | bigint> =
`${T}` extends `-${infer R}`
? R
: `${T}`
いけた。
531. String to Union
文字列を1文字ずつのunion型にする。こう。
type StringToUnion<T extends string> =
T extends `${infer F}${infer R}`
? F | StringToUnion<R>
: never;
Expect<Equal<StringToUnion<''>, never>>
がヒントになった。
次のように文字列のテンプレートリテラル型で infer を使って推論するとき、Fは初めの1文字、Rは残りの文字列にマッチングするという仕組み。
T extends `${infer F}${infer R}` ? ... : ...
599. Merge
オブジェクトのフィールドをマージする。2つ目の要素の型で上書きするからこんな感じかな。
type Merge<F, S> = {
[K in keyof F | keyof S]:
K extends keyof S
? S[K]
: K extends keyof F
? F[K]
: never
}
612. KebabCase
アルファベットの集合を型として表現した方が良い気がしたので、string to union を使うか。
type StringToUnion<T extends string> =
T extends `${infer F}${infer R}`
? F | StringToUnion<R>
: never;
で、こうやる。
type StringToUnion<T extends string> =
T extends `${infer F}${infer R}`
? F | StringToUnion<R>
: never;
type Cap = StringToUnion<'ABCDEFGHIJKLMNOPQRSTUVWXYZ'>;
// 2文字目以降はこれで処理する
type MidKebabCase<S> =
S extends `${infer F}${infer R}`
? F extends Cap
? `-${Lowercase<F>}${MidKebabCase<R>}`
: `${F}${MidKebabCase<R>}`
: '';
type KebabCase<S> =
S extends `${infer F}${infer R}`
? F extends Cap
? `${Lowercase<F>}${MidKebabCase<R>}` // 先頭の文字は - は不要。
: `${F}${MidKebabCase<R>}`
: '';
解答を見ます。これ多分自力で思いつけない。読み解くか。
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<S>
は S
の先頭一文字を小文字に変換する。つまり S2
が小文字始まりであればそのまま再帰的に検査を行い、小文字でなければ -
を追加して KebabCase<S2>
を当てる。
ミソは Uncapitalize<S1>
だな。大文字を小文字に変換して直前に -
をつける脳だったけど、大文字から小文字への変換と -
の付与を別で行えばいい。
S1
は1文字だけしか拾わないので、 Lowercase
でも良い。個人的にはこっちの方が理解しやすい。
type KebabCase<S extends string> = S extends `${infer S1}${infer S2}`
? S2 extends Uncapitalize<S2>
? `${Lowercase<S1>}${KebabCase<S2>}`
: `${Lowercase<S1>}-${KebabCase<S2>}`
: S;
645. Diff
よくわからん。
type DiffKey<O, O1> =
Exclude<keyof O, keyof O1> | Exclude<keyof O1, keyof O>;
type Diff<O, O1> = {
[K in DiffKey<O, O1>]:
K extends keyof O
? O[K]
: K extends keyof O1
? O1[K]
: never
};
解答みた。これでええんか。 keyof (O | O1)
の使い方が独特。
type Diff<O, O1> = Omit<O & O1, keyof (O | O1)>
949. AnyOf
タプル型のいずれかの要素が真なら真にする。Faslyな値を定義してtuple型の要素を一つずつチェックする。
type Falsy = false | 0 | "" | null | undefined | Record<any, never> | []
type AnyOf<T extends readonly any[]> =
T extends [infer F, ...infer R]
? F extends Falsy
? AnyOf<R>
: true
: false
回答を見た。なるほど、 T[number]
でUnionTypeに変換して、Falsyな要素のみのUnion Typeであれば extends を通るのか。
type AnyOf<T extends any[]> =
T[number] extends 0 | '' | false | [] | { [key: string]: never }
? false : true;
1042. IsNever
一見シンプル。
で、こうやってみたら IsNever<never>
が never
になっていてわろた。
type IsNever<T> = T extends never ? true : false
ヒントがないかとググってたら答えに辿り着いてしまった。
type IsNever<T> = T[] extends never[] ? true : false
never は評価されないから、評価できる値に置き換えるのか。
1097. IsUnion
Union Typeの判定か。Union to Tuple して長さを検査する方針で。
で、neverの判定にIsNeverを使う。
type Permutation<T, Orig=T> =
[T] extends [never]
? []
: T extends unknown
? [T, ...Permutation<Exclude<Orig, T>>]
: never
type IsNever<T> = T[] extends never[] ? true : false
type IsUnion<T> =
IsNever<T> extends true
? false
: Permutation<T>['length'] extends 1
? false
: true
これで通った。
解答みた。難解。
type IsUnionImpl<T, C extends T = T> = (T extends T ? C extends T ? true : unknown : never) extends true ? false : true;
type IsUnion<T> = IsUnionImpl<T>;
ちょっと書き換える。
type IsSingleImpl<T, C = T> =
T extends T ? C extends T ? true : unknown : never;
type IsUnion<T> = IsSingleImpl<T> extends true ? false : true
IsSingleImpl<T, C = T>
がミソなのか。まず T extends T
は never
の判定に使っている。で、型引数TとCを比較する際、TがUnion Typeの場合は C extends T
は false 側に倒れるから、それを使ってUnionTypeかどうかの場合分けができる。
Tが Union Type の場合に C extends T
が false になるのは、distributed conditional type の性質を使っている。つまり、T extends T
の時点で T が Union Type であれば分割して評価されるから、その後ろの C extends T
の T は分割済みの型となる。C(=T) もまたUnion Typeだから、ここでまた分割が走る。結果、分割後の型同士を総当たり的に比較するため、C extends T
が成り立たず unknown
に倒れるケースが存在する。これをUnion Typeに統合すると、true | ... | unknown | ...
となり、最終的に unkonwn
に倒れる。
自分の理解を書くために文で説明したけど、解答例の例示がちゃんとしているのでそっちをみた方が良い。
1130. ReplaceKeys
キーに対応する型を置き換えたいっぽい。こうしてみたが1つめのケースが通らない。
type ReplaceKeys<U, T extends keyof any, Y> =
U extends { [K in T]: any }
? { [K in keyof U]: K extends T ? K extends keyof Y ? Y[K] : never : U[K] }
: U
よく考えたら U extends { [K in T]: any }
の時点でサンプルケースの NodeB
の置き換えが止まっちゃうから、この条件分岐はいらないな。
type ReplaceKeys<U, T extends keyof any, Y> =
{ [K in keyof U]: K extends T ? K extends keyof Y ? Y[K] : never : U[K] }
通った。コメント入れてみる。
type ReplaceKeys<U, T extends keyof any, Y> = {
[K in keyof U]:
K extends T // 置き換え対象のキーかチェックする
? K extends keyof Y // 置き換え先のキー情報が存在するかチェックする
? Y[K] // 置き換え
: never // 置き換え先が存在しないから never に
: U[K] // 置き換えしない
}
解答と同じだった。OK
1367. Remove Index Signature
index signature ([key: string]: any
みたいなやつ) を取り除く。
type RemoveIndexSignature<T> = { [K in keyof T]: T[K] }
もしかしたら通るかもと思ったけど、流石にこれではダメだった。index signature を見分ける方法が思いつかない。
というわけで解答を見る。
type RemoveIndexSignature<T, P = PropertyKey> = {
[K in keyof T as P extends K ? never : K extends P ? K : never]: T[K];
};
一回 K
側を絞り込もうと思ってうまく行かないなと思ったけど、やり方はあったのか。
PropertyKey
は組み込み型で string | number | symbol
だった。
-
P extends K
の評価時にK
がリテラルだったらfalse
になる。一方、K
がindex signatureつまりstring
,number
,symbol
のいずれかだった場合はP
のdistributed condition typeの評価の結果true
になり、never
に吸い込まれる -
P
に関する分割の先でK extends P
を評価することで、index signatureが全部潰せるっぽい。ちゃんと評価の順番を書き下すと理解できそう。
1978. Percentage Parser
パーセント表記をパースしたい。めっちゃボトムアップで組み立てたが、だめ。
type PlusMinus = '+' | '-' | '';
type Percent = '%' | '';
type NumberParser<T extends string> =
T extends `${infer N extends number}${infer R}`
? `${N}${NumberParser<R>}`
: ``;
type _PercentageParser<A extends string, PM = ''> =
A extends `${infer N extends NumberParser<A>}${infer R1 extends Percent}`
? [PM, N, R1]
: A extends `${infer R2 extends Percent}`
? [PM, '', R2]
: [PM, '', ''];
type PercentageParser<A extends string> =
A extends `${infer PM extends PlusMinus}${infer R1}`
? _PercentageParser<R1, PM>
: _PercentageParser<A>;
_PercentageParser<A, PM>
の最初の条件分岐で Percent が潰れてしまう。
回答見たら非常にうまいと思った。
type CheckPrefix<T> = T extends '+' | '-' ? T : never;
type CheckSuffix<T> = T extends `${infer P}%` ? [P, '%'] : [T, ''];
type PercentageParser<A extends string> = A extends `${CheckPrefix<infer L>}${infer R}` ? [L, ...CheckSuffix<R>] : ['', ...CheckSuffix<A>];
2070. Drop Char
文字列型の操作をするシリーズ。
愚直に1文字ずつ走査するか。
type DropChar<S extends string, C extends string> =
S extends `${infer F}${infer R}`
? F extends C
? `${DropChar<R, C>}`
: `${F}${DropChar<R, C>}`
: ''
解答みた。確かにそうだな。
type DropChar<S, C extends string> =
S extends `${infer L}${C}${infer R}`
? `${L}${DropChar<R, C>}`
: S
手続き的に解こうとしてしまうなー。
2257. MinusOne
これって整数の定義の話?わからないから早速回答を見た。
すげえ。
数値型だと何もできないから、一回文字列型に変換して逆順に一文字ずつ操作していくってことね。自分でも組んでみよう。
多分同じ形になった
type NumStringArray = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
type NumString = NumStringArray[number];
type ToNumber<T extends string> =
T extends `${infer N extends number}` ? N : T;
type ExcludeZero<T extends string> =
T extends `0${infer R}`
? R extends ''
? '0'
: ExcludeZero<R>
: T;
type ReverseString<T> =
T extends `${infer F}${infer R}` ? `${ReverseString<R>}${F}` : T;
type _MinusOne<T extends string> =
T extends `${infer N extends NumString}${infer R}`
? N extends 0
? `9${_MinusOne<R>}`
: `${['_', ...NumStringArray][N]}${R}`
: T;
type MinusOne<T extends number> =
ToNumber<ExcludeZero<ReverseString<_MinusOne<ReverseString<`${T}`>>>>>;
2595. PickByType
From T, pick a set of properties whose type are assignable to U.
なんとなく簡単に見えるけどどうなんだろう。
まずこれはダメな例。U に当てはまらない場合は key: never
が入ってしまう。
type PickByType<T extends Record<PropertyKey, any>, U> = {
[K in keyof T]: T[K] extends U ? T[K] : never
}
キーを絞り込みたいけど、いい条件式が書けない。回答を見た。
type PickByType<T extends Record<PropertyKey, any>, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K]
}
[K in keyof T as ... ] の意味がわかってないぜ。キーの型を定義するときに as
以降の型を採用するってことなんかな。
2688. StartsWith
第1型引数の文字列リテラル型の文字列が、第2型引数の文字列リテラル型の文字列から始まるかどうかを判定する。
ここにきて単純な問題が。
type StartsWith<T extends string, U extends string> =
T extends `${U}${string}` ? true : false
2693. EndsWith
type EndsWith<T extends string, U extends string> =
T extends `${string}${U}`
? true
: false
この問題、もっと先に出すべきでは?
2757. PartialByKeys
思ったより難しい。
まずこれはダメ。意味合いは同じだけどoptionalにはならない。
type PartialByKeys<T, K extends PropertyKey> = {
[P in keyof T]: P extends K ? T[P] | undefined : T[P]
}
次にこれもダメ。Not Partialではない要素が落ちる。
type PartialByKeys<T, K extends PropertyKey> = {
[P in keyof T as P extends K ? P : never]?: T[P]
}
mapped type のキー []
の外側だもんな。intersectionでいけるかと思ったけどダメ。
type PartialByKeys<T, K extends PropertyKey> = {
[P in keyof T as P extends K ? P : never]?: T[P]
} & {
[P in keyof T as P extends K ? never : P]: T[P]
}
あと気にしてなかったけど、Kは K = keyof T
を初期値にすれば多分OK。
type PartialByKeys<T, K = keyof T> = ...
答えみた。intersectionを単一オブジェクトに変えればいいのか。で、Omit<T, never>
でそれが実現できると。
type PartialByKeys<T, K extends keyof T = keyof T> =
Omit<{
[P in keyof T as P extends K ? P : never]?: T[P]
} & {
[P in Exclude<keyof T, K>]: T[P]
}, never>
K
に制約をつけることで ts-expect-error も通った。見通しを良くするため、Omitの中身を括り出した。
type _PartialByKeys<T, K> = {
[P in keyof T as P extends K ? P : never]?: T[P]
} & {
[P in Exclude<keyof T, K>]: T[P]
}
type PartialByKeys<T, K extends keyof T = keyof T> = Omit<_PartialByKeys<T, K>, never>
2759. RequiredByKeys
PartialByKeysの逆かな。こうなった。
type _Inner<T, K> = {
[P in keyof T as P extends K ? P : never]-?: T[P];
} & {
[P in keyof T as P extends K ? never : P]: T[P];
}
type RequiredByKeys<T, K extends keyof T = keyof T> = Omit<_Inner<T, K>, never>
mapped type で Kに当てはまらないkeyを抽出するところで [P in Exclude<keyof T, K>]
と指定するとなぜかoptional属性が外れる。
2793. Mutable
やっぱ問題の順序違うんじゃない?
type Mutable<T extends Record<keyof any, any>> = {
-readonly [K in keyof T]: T[K]
}
2852. OmitByType
これももっと早く出すべきでは?
type OmitByType<T, U> = {
[K in keyof T as T[K] extends U ? never : K]: T[K]
}
2946. ObjectEntries
急になんか難しそうな感じが。再帰使って泥臭く定義していくか?
なんとなくこう書いたけど、当然ダメ。 extends がそもそもObjectの要件を満たしてない
type ObjectEntries<T> =
T extends [{[P in infer K]: infer V}, ... infer R]
? [K, V] | ObjectEntries<R>
: never
keyof T をうまくこねくり回すことで成立させた。
type ObjectEntries<T extends Record<keyof any, any>> =
keyof T extends infer R
? R extends keyof T
? T[R] extends infer R1 | undefined
? [R, R1]
: [R, T[R]]
: []
: [];
optionalなフィールドをうまく捌くために T[R] extends infer R1 | undefined
としたが、ダメでした。もとより hoge | undefined
な型のフィールドから undefined
を落としてしまう。
さらに試行錯誤して、こうなった。{ key?: undefined }
がかなりコーナーケースだったので、専用の条件分岐を作っている。
type ObjectEntries<T extends Record<keyof any, any>> =
keyof T extends infer R
? R extends keyof T
? T[R] extends undefined
? [R, undefined]
: [R, Required<T>[R]]
: []
: [];
回答みたらだいぶ頭良さげだった。
T[keyof T]
を使うと value の union type が取り出せるのを使うのね。
type _ObjectEntries<T extends Record<keyof any, any>> = {
[K in keyof T]: [K, T[K]]
}[keyof T]
これに undefined 対策を入れればOK。
あと自分の考え方と近い回答もあった。初期値に keyof T
を持つ型引数 K を用意することで、 keyof T extends infer R ? R extends keyof T
の条件式を K extends K
まで省略できる。なお K extends K
はUnion Typeを分割評価するために必須。
type ObjectEntries<T extends Record<keyof any, any>, K extends keyof T = keyof T> =
K extends K
? [K, T[K] extends undefined ? undefined : Required<T>[K]]
: never
3062. Shift
ベタに再帰で書けるか?
type Shift<T extends readonly any[]> = T extends [unknown, ...(infer R)] ? R : [];
これでいいな。
3188. Tuple to Nested Object
なんか無理やり作った問題に見える。
type TupleToNestedObject<T extends readonly any[], U> =
T extends [infer F extends string, ...(infer R)]
? { [K in F]: TupleToNestedObject<R, U> }
: U
3192. Reverse
解き方は単純そう。
type Reverse<T extends readonly any[]> =
T extends [infer F, ...(infer R)] ? [...Reverse<R>, F] : []
最後は T
の方が適切そうだった。多分変わらんけど。
3196. FlipArguments
これも割と簡単そう。
と思ったけどうまくいかない...
type Reverse<T extends any[]> =
T extends [infer F, ...infer R] ? [...Reverse<R>, F] : T
type FlipArguments<T extends Function> =
T extends (...args: infer A) => infer R
? (args: Reverse<A>) => R
: T;
これだと引数が args に丸められてしまう。と思ったらspreadが足りないだけだった。
type Reverse<T extends any[]> =
T extends [infer F, ...infer R] ? [...Reverse<R>, F] : T
type FlipArguments<T extends Function> =
T extends (...args: infer A) => infer R
? (...args: Reverse<A>) => R
: T;
3243. FlattenDepth
数字のリテラル型をそのまま数字として扱えないのが厳しい。タプルの要素数とか組み合わせればいいんかな?
良い方法がわからなかったから、安直に MinuOne と組み合わせる。
type NumStringArray = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
type NumString = NumStringArray[number];
type ToNumber<T extends string> =
T extends `${infer N extends number}` ? N : T;
type ExcludeZero<T extends string> =
T extends `0${infer R}`
? R extends ''
? '0'
: ExcludeZero<R>
: T;
type ReverseString<T> =
T extends `${infer F}${infer R}` ? `${ReverseString<R>}${F}` : T;
type _MinusOne<T extends string> =
T extends `${infer N extends NumString}${infer R}`
? N extends 0
? `9${_MinusOne<R>}`
: `${['_', ...NumStringArray][N]}${R}`
: T;
type MinusOne<T extends number> =
ToNumber<ExcludeZero<ReverseString<_MinusOne<ReverseString<`${T}`>>>>>;
type FlattenDepth<T extends any[], U extends number = 1> =
U extends 0 ? T
: T extends [infer F, ...infer R]
? F extends any[] ? [...FlattenDepth<F, MinusOne<U>>, ...FlattenDepth<R, U>] : [F, ...FlattenDepth<R, U>]
: T
回答を見た。やっぱタプルと組み合わせるといい感じにできるんか。カウントダウンする方向で考えていたけど、要素を足していって要素数を比較すれば良かったのか。
type FlattenDepth<
T extends any[],
S extends number = 1,
U extends any[] = []
> = U['length'] extends S
? T
: T extends [infer F, ...infer R]
? F extends any[]
? [...FlattenDepth<F, S, [...U, 1]>, ...FlattenDepth<R, S, U>]
: [F, ...FlattenDepth<R, S, U>]
: T
3326. BEM style string
template literal types で組み合わせを考えていけば多分どうにかなる?
TypeScript: Documentation - Template Literal Types
全パターンを網羅しようとするとこうなった
type BEM<B extends string, E extends string[], M extends string[]> =
E extends [infer EF extends string, ...infer ER extends string[]]
? M extends [infer MF extends string, ...infer MR extends string[]]
? `${B}__${EF}--${MF}` | BEM<B, ER, [MF]> | BEM<B, ER, MR>
: `${B}__${EF}`
: M extends [infer MF extends string, ...infer MR extends string[]]
? `${B}--${MF}` | BEM<B, [], MR>
: `${B}`
が、これだと M
の有無に関わらず btn
や btn__price
が必ず入ってテストケースが落ちる。
場合分けで頭がこんがらがってきたので、一旦答えを見る。天才かよ。
type BEM<B extends string, E extends string[],M extends string[]> =
`${B}${E extends [] ? '' : `__${E[number]}`}${M extends [] ? '' : `--${M[number]}`}`
3376. InorderTraversal
val
left
right
の木構造を追いかけてソートしていくのね。型でやることじゃないw
雑に再帰で書いてみたら、 Type instantiation is excessively deep and possibly infinite.(2589)
と Expression produces a union type that is too complex to represent.(2590)
で怒られた。
interface TreeNode {
val: number
left: TreeNode | null
right: TreeNode | null
}
type InorderTraversal<T extends TreeNode | null> =
T extends null
? []
: T extends { val: infer N; left: infer L extends TreeNode | null; right: infer R extends TreeNode | null }
? [...InorderTraversal<L>, N, ...InorderTraversal<R>]
: [];
left, right の型を TreeNode
と null
で場合分けしたら通った。なるほど再帰的な型定義を走査するときは場合分けをする必要があるのか。
interface TreeNode {
val: number
left: TreeNode | null
right: TreeNode | null
}
type InorderTraversal<T extends TreeNode | null> =
T extends null
? []
: T extends { left: null; val: infer N; right: null }
? [N]
: T extends { left: infer L extends TreeNode; val: infer N; right: null }
? [...InorderTraversal<L>, N]
: T extends { left: null, val: infer N; right: infer R extends TreeNode }
? [N, ...InorderTraversal<R>]
: T extends { left: infer L extends TreeNode, val: infer N; right: infer R extends TreeNode }
? [...InorderTraversal<L>, N, ...InorderTraversal<R>]
: []
解答例を見た。頭いいね。
- 型引数を増やすことで複雑性のエラーを回避している?
- NonNullable の役割は?
interface TreeNode {
val: number;
left: TreeNode | null;
right: TreeNode | null;
}
type InorderTraversal<T extends TreeNode | null, NT extends TreeNode = NonNullable<T>> = T extends null
? []
: [...InorderTraversal<NT['left']>, NT['val'], ...InorderTraversal<NT['right']>]
4179. Flip
any 使って無理やり通した。 anyを使わない場合は ${T[K]}
でエラーになる。
type Flip<T extends Record<PropertyKey, any>> = {
[
K in keyof T as K extends PropertyKey
? T[K] extends PropertyKey
? T[K]
: `${T[K]}`
: never
]: K
}
解答例みた。こんな複雑なことしなくてもよかったw
key remappingの理解が足りない。
type Flip<T extends Record<PropertyKey, any>> = {
[P in keyof T as `${T[P]}`]: P
}
4182. Fibonacci Sequence
また数字を扱う問題だ。型で数値を扱うアイデアを過去の問題から引っ張るか。
FlattenDepthではタプルの長さを使って数字を表現していた。
同じ方針でやってみようかな。フィボナッチ数列は基本的に再帰で実現できるけど、カウントアップするなら2つ前までの数字を覚えながらカウントアップしていくのが良いかも。
というわけでこうやったところ、FibonacciTuple<>['length']
でタプルの長さがえぐいことになるでと怒られた。
type FibonacciTuple<
T extends number,
C extends any[] = [], // カウンター
P1 extends any[] = [], // 1つ前の値を意味する配列
P2 extends any[] = [] // 2つ前の値を意味する配列
> = C['length'] extends T
? [...P1, ...P2]
: C['length'] extends 0
? FibonacciTuple<T, [...C, 1], [1], []>
: C['length'] extends 1
? FibonacciTuple<T, [...C, 1], [1], []>
: FibonacciTuple<T, [...C, 1], [...P1, ...P2], [...P1]>
type Fibonacci<T extends number> = FibonacciTuple<T>['length']
解答を見たらもっとシンプルだったので書き直してみる。初期値を適切に設定している。
type Fibonacci<
T extends number,
C extends any[] = [1], // カウンター
Current extends any[] = [1], // 現在値
Prev extends any[] = [] // 1つ前の値
> = C['length'] extends T
? Current['length']
: Fibonacci<T, [...C, 1], [...Current, ...Prev], Current>