Open50

Type Challenges ログ

HikosaburouHikosaburou

過去ログ 2025/04/30

slackに色々書いてたメモを転記する。

昨日はこれやってた
https://github.com/type-challenges/type-challenges/blob/main/questions/00898-easy-includes/README.ja.md

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
  • タプルTの全要素の型について IsEqual で判定を行う
    • 再帰を使うと [] の要素と undefined が一致してしまうため、 Includes<[null], undefined> の判定にこける
    • タプルから新しく boolean[] な型を生成して false[] との一致を比較する
HikosaburouHikosaburou

過去ログ 5/6

1週間ぶり


前回はIncludesだったはずなので、Pushから
https://github.com/type-challenges/type-challenges/blob/main/questions/03057-easy-push/README.ja.md


めちゃしんぷるだった

type Push<T, U> = T extends readonly any[] ?  [...T, U] : never;

Unshift も簡単だったらいいな
https://github.com/type-challenges/type-challenges/blob/main/questions/03060-easy-unshift/README.ja.md


めっちゃ簡単だった

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

Pushの時は型定義のタイミングで extends で条件分岐したけど、型変数の段階で絞り込んだ方が親切だな。

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

Parametersはちょっとkeiroが違う
https://github.com/type-challenges/type-challenges/blob/main/questions/03312-easy-parameters/README.ja.md


infer で推論してそれを取り出す

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

組み込みで Parameters っていう型があるんだな。

初級編終わった。Includesだけ妙に複雑だったな。

HikosaburouHikosaburou

過去ログ 2025/05/08

Deep Readonly これは再帰的な型定義が必要になるやつかな
https://github.com/type-challenges/type-challenges/blob/main/questions/00009-medium-deep-readonly/README.ja.md


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

過去ログ 2025/05/07

Get Return Type
https://github.com/type-challenges/type-challenges/blob/main/questions/00002-medium-return-type/README.ja.md

infer の例題っぽい。

type MyReturnType<T> = T extends (...args: any[]) => (infer U) ? U : never;

TSの実装はこれかしら。
https://github.com/microsoft/TypeScript/blob/d88d3a46810bfae0b072beb023a2b09b8026b82c/src/lib/es5.d.ts#L1628

確かに関数の引数は any[] で絞らなくて良いな。


Omit
https://github.com/type-challenges/type-challenges/blob/main/questions/00003-medium-omit/README.ja.md

この辺までは前にやった覚えがあるな。

わかんないから答え見てしまった

うまくいく例

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 で繋げば良い的な話?
https://github.com/type-challenges/type-challenges/blob/main/questions/00008-medium-readonly-2/README.ja.md

最終的にこうなった

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 するのが重要
  • { 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>>
HikosaburouHikosaburou

過去ログ 2025/05/09

Tuple to Union
https://github.com/type-challenges/type-challenges/blob/main/questions/00010-medium-tuple-to-union/README.ja.md

number 使えば ok のはず

type TupleToUnion<T extends readonly any[]> = T[number]


解説記事があった。斜め読みだけした。
https://zenn.dev/luvmini511/articles/d89b3ad241e544

実は前の問題でタプルについて推論していた。(当然忘れている

つまりタプルはsrting integerをキーに持つオブジェクトということ?

HikosaburouHikosaburou

過去ログ 2025/05/12

Chainable Options
https://github.com/type-challenges/type-challenges/blob/main/questions/00012-medium-chainable-options/README.ja.md

これはなんかややこしそうだな。

雑に考えて書いてみた。

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を使う。
    • が、そもそもキーの重複を許容しないという条件があるのでここは本質的ではない。
HikosaburouHikosaburou

過去ログ 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への変換が難しそうという記事を見た。

TypeScript で union to tuple をするのが難しい理由

HikosaburouHikosaburou

20 Promise.all

type-challenges/questions/00020-medium-promise-all/README.ja.md at main · type-challenges/type-challenges · GitHub

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

というわけでこうしてみた。

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も使えた。

HikosaburouHikosaburou

62. Type Lookup

type-challenges/questions/00062-medium-type-lookup/README.ja.md at main · type-challenges/type-challenges · GitHub

この問題の設定に限るのであればこれで良いよな。でも流石に違うはず。

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
HikosaburouHikosaburou

106. Trim Left

type-challenges/questions/00106-medium-trimleft/README.ja.md at main · type-challenges/type-challenges · GitHub

文字列を受け取り、先頭の空白を削除した新しい文字列を返す 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
HikosaburouHikosaburou

110. Capitalize

type-challenges/questions/00110-medium-capitalize/README.ja.md at main · type-challenges/type-challenges · GitHub

文字列の最初の文字を大文字に変換し、それ以外はそのままにする Capitalize<T> を実装します。

なんだと。

https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html#capitalizestringtype

すでに組み込み型がある。

そして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

通った。

回答を見る。

https://github.com/type-challenges/type-challenges/issues/759

Uppercase<Stringtype> 使えるやんけ

type MyCapitalize<S extends string> =
  S extends `${infer F}${infer R}`
    ? `${Uppercase<F>}${R}`
    : S;

あとこれは勉強になる。テンプレート型でinfer使って推論する時の判定方法。

https://github.com/type-challenges/type-challenges/issues/759#issuecomment-2132540737

HikosaburouHikosaburou

116. Replace

type-challenges/questions/00116-medium-replace/README.ja.md at main · type-challenges/type-challenges · GitHub

文字列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

わざわざ再帰使わなくてもよかったやん。なるほどね。

HikosaburouHikosaburou

119. ReplaceAll

type-challenges/questions/00119-medium-replaceall/README.ja.md at main · type-challenges/type-challenges · GitHub

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}'' も含むからこれでいける。

HikosaburouHikosaburou

191. Append Argument

type-challenges/questions/00191-medium-append-argument/README.ja.md at main · type-challenges/type-challenges · GitHub

与えられた関数型 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
HikosaburouHikosaburou

296. Permutation

type-challenges/questions/00296-medium-permutation/README.ja.md at main · type-challenges/type-challenges · GitHub

permutation: 順列、交換、置換、並べ換え (英語「permutation」の意味・使い方・読み方 | Weblio英和辞書)

Union 型を Union 型の値の順列を含む配列に変換する順列型を実装します。

Union to Tupleだ。なんか難しそうだけど、前に一回こんな形の型を作ったことがある気がする。

まずはオブジェクトとして取り出してみる。

type Permutation<T extends string> = {[Key in T]: Key}

これは単に { A: 'A'} みたいなオブジェクト型になるだけ。

前に調べたのはこれだ。

https://zenn.dev/link/comments/c70126e98801b5
https://zenn.dev/dqn/articles/union-to-tuple

まんま順列の実装が書いてあった。

type Permutation<T, Orig=T> = 
  [T] extends [never]
  ? []
  : T extends unknown
  ? [T, ...Permutation<Exclude<Orig, T>>]
  : never

ただ、なんでこうなるかが理解できない。解答の解説を読む。

https://github.com/type-challenges/type-challenges/issues/614#issuecomment-790210311

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' ] みたいな評価が入るってことか。

実際に試してみたらその通りだった。

HikosaburouHikosaburou

459. Flatten

type-challenges/questions/00459-medium-flatten/README.ja.md at main · type-challenges/type-challenges · GitHub

タプル型のネストを取り除いてフラット化する

とりあえず要素を一つずつ推論して、再帰的に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

多分もっと短くなると思ったけど、これでも良さそうだった。

https://github.com/type-challenges/type-challenges/issues/1314

HikosaburouHikosaburou

527. Append to object

type-challenges/questions/00527-medium-append-to-object/README.ja.md at main · type-challenges/type-challenges · GitHub

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

解答例を見る。
https://github.com/type-challenges/type-challenges/issues/536

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

529. Absolute

type-challenges/questions/00529-medium-absolute/README.ja.md at main · type-challenges/type-challenges · GitHub

絶対値への変換となっているが、実質は数値等から文字列型への変換。

文字列のテンプレートリテラル型を使えばいけるのでは、と思った。まずは単純な文字列への変換。

type Absolute<T extends number | string | bigint> = `${T}`;

文字列に変換した時に '-' が先頭に来たら取り除けば良い?

type Absolute<T extends number | string | bigint> = 
  `${T}` extends `-${infer R}`
  ? R
  : `${T}`

いけた。

HikosaburouHikosaburou

531. String to Union

type-challenges/questions/00531-medium-string-to-union/README.ja.md at main · type-challenges/type-challenges · GitHub

文字列を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}` ? ... : ...
HikosaburouHikosaburou

612. KebabCase

type-challenges/questions/00612-medium-kebabcase/README.ja.md at main · type-challenges/type-challenges · GitHub

アルファベットの集合を型として表現した方が良い気がしたので、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;
HikosaburouHikosaburou

949. AnyOf

type-challenges/questions/00949-medium-anyof/README.ja.md at main · type-challenges/type-challenges · GitHub

タプル型のいずれかの要素が真なら真にする。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 を通るのか。

https://github.com/type-challenges/type-challenges/issues/954

type AnyOf<T extends any[]> =
  T[number] extends 0 | '' | false | [] | { [key: string]: never }
  ? false : true;
HikosaburouHikosaburou

1042. IsNever

type-challenges/questions/01042-medium-isnever/README.md at main · type-challenges/type-challenges · GitHub

一見シンプル。

で、こうやってみたら IsNever<never>never になっていてわろた。

type IsNever<T> = T extends never ? true : false

ヒントがないかとググってたら答えに辿り着いてしまった。

type IsNever<T> = T[] extends never[] ? true : false

never は評価されないから、評価できる値に置き換えるのか。

HikosaburouHikosaburou

1097. IsUnion

type-challenges/questions/01097-medium-isunion/README.md at main · type-challenges/type-challenges · GitHub

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 Tnever の判定に使っている。で、型引数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 に倒れる。

自分の理解を書くために文で説明したけど、解答例の例示がちゃんとしているのでそっちをみた方が良い。

HikosaburouHikosaburou

1130. ReplaceKeys

type-challenges/questions/01130-medium-replacekeys/README.md at main · type-challenges/type-challenges · GitHub

キーに対応する型を置き換えたいっぽい。こうしてみたが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

HikosaburouHikosaburou

1367. Remove Index Signature

type-challenges/questions/01367-medium-remove-index-signature/README.md at main · type-challenges/type-challenges · GitHub

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が全部潰せるっぽい。ちゃんと評価の順番を書き下すと理解できそう。
HikosaburouHikosaburou

1978. Percentage Parser

type-challenges/questions/01978-medium-percentage-parser/README.md at main · type-challenges/type-challenges · GitHub

パーセント表記をパースしたい。めっちゃボトムアップで組み立てたが、だめ。

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

2070. Drop Char

type-challenges/questions/02070-medium-drop-char/README.md at main · type-challenges/type-challenges · GitHub

文字列型の操作をするシリーズ。

愚直に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

手続き的に解こうとしてしまうなー。

HikosaburouHikosaburou

2257. MinusOne

type-challenges/questions/02257-medium-minusone/README.md at main · type-challenges/type-challenges · GitHub

これって整数の定義の話?わからないから早速回答を見た。

https://github.com/type-challenges/type-challenges/issues/13507

すげえ。

数値型だと何もできないから、一回文字列型に変換して逆順に一文字ずつ操作していくってことね。自分でも組んでみよう。


多分同じ形になった

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}`>>>>>;
HikosaburouHikosaburou

2595. PickByType

type-challenges/questions/02595-medium-pickbytype/README.md at main · type-challenges/type-challenges · GitHub

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 以降の型を採用するってことなんかな。

HikosaburouHikosaburou

2757. PartialByKeys

type-challenges/questions/02757-medium-partialbykeys/README.md at main · type-challenges/type-challenges · GitHub

思ったより難しい。

まずこれはダメ。意味合いは同じだけど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>
HikosaburouHikosaburou

2759. RequiredByKeys

type-challenges/questions/02759-medium-requiredbykeys/README.md at main · type-challenges/type-challenges · GitHub

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属性が外れる。

HikosaburouHikosaburou

2946. ObjectEntries

type-challenges/questions/02946-medium-objectentries/README.md at main · type-challenges/type-challenges · GitHub

急になんか難しそうな感じが。再帰使って泥臭く定義していくか?

なんとなくこう書いたけど、当然ダメ。 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]]
    : []
  : [];

回答みたらだいぶ頭良さげだった。

https://github.com/type-challenges/type-challenges/issues/14052

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
HikosaburouHikosaburou

3196. FlipArguments

type-challenges/questions/03196-medium-flip-arguments/README.md at main · type-challenges/type-challenges · GitHub

これも割と簡単そう。

と思ったけどうまくいかない...

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;
HikosaburouHikosaburou

3243. FlattenDepth

type-challenges/questions/03243-medium-flattendepth/README.md at main · type-challenges/type-challenges · GitHub

数字のリテラル型をそのまま数字として扱えないのが厳しい。タプルの要素数とか組み合わせればいいんかな?

良い方法がわからなかったから、安直に 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
HikosaburouHikosaburou

3326. BEM style string

type-challenges/questions/03326-medium-bem-style-string/README.md at main · type-challenges/type-challenges · GitHub

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 の有無に関わらず btnbtn__price が必ず入ってテストケースが落ちる。

場合分けで頭がこんがらがってきたので、一旦答えを見る。天才かよ。

type BEM<B extends string, E extends string[],M extends string[]> = 
  `${B}${E extends [] ? '' : `__${E[number]}`}${M extends [] ? '' : `--${M[number]}`}`
HikosaburouHikosaburou

3376. InorderTraversal

type-challenges/questions/03376-medium-inordertraversal/README.md at main · type-challenges/type-challenges · GitHub

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 の型を TreeNodenull で場合分けしたら通った。なるほど再帰的な型定義を走査するときは場合分けをする必要があるのか。

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

4179. Flip

type-challenges/questions/04179-medium-flip/README.md at main · type-challenges/type-challenges · GitHub

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
}
HikosaburouHikosaburou

4182. Fibonacci Sequence

type-challenges/questions/04182-medium-fibonacci-sequence/README.md at main · type-challenges/type-challenges · GitHub

また数字を扱う問題だ。型で数値を扱うアイデアを過去の問題から引っ張るか。

FlattenDepthではタプルの長さを使って数字を表現していた。

https://zenn.dev/link/comments/7b6852053b8a4b

同じ方針でやってみようかな。フィボナッチ数列は基本的に再帰で実現できるけど、カウントアップするなら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>