Open99

type-challenges 学習録

high-ghigh-g

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

進め方

https://qiita.com/ryo2132/items/925b96838dd8cca7cebd

参考

https://zenn.dev/yutake27/articles/6d117390805af0
https://dev.classmethod.jp/articles/type-challenges-easy-try/
https://zenn.dev/azukiazusa/scraps/7f3396b6e6cfab

手順

  1. READMEから挑戦した問題を選ぶ
    バッジをクリックすると問題詳細ファイルに移動

  2. 「TS 挑戦する」ボタンを押すと、TypeScript Playgroundで問題にチャレンジすることができる

  3. 回答を共有 or 解答を確認

Pickをやってみる

Pick<T, Keys>のユーティリティ関数の挙動自体を知らなかった。。
TS弱者を自覚。。(今まで雰囲気でTSを書いていた。)
Tの型からKeysで指定したキーだけを含むオブジェクト型を返すユーティリティ型とのこと。

type MyPick<T, K> = any

easyはTSの基本文法を知る為の問題らしい。
文法的知識がないから取り掛かりがわからん。。

PickよりもIfが良いみたい。
先にそっちを解こう。

high-ghigh-g

If

https://github.com/type-challenges/type-challenges/blob/main/questions/00268-easy-if/README.md

Ifで必要な知識は以下とのこと

型引数の制約
Conditional Types

型引数の制約

要約:ジェネリクスので受け取る型を制限する方法 extends を使う

ジェネリクス型引数で直面する問題

function changeBackgroundColor<T>(element: T) {
  element.style.backgroundColor = 'red'
  // Property 'style' dose not exist on type 'T'
  return element
}

ジェネリクス型Tは任意の型の指定が可能になっているコードの為、
上記はエラーで失敗する。
渡す型によっては、styleプロパティが存在しない場合が存在するから。

function changeBackgroundColor<T>(element: T) {
  // any型だとコンパイルエラーは回避できる
  // 型チェックされないためバグの可能性
  (element as any).style.backgroundColor = 'red'
  return element
}

extends キーワードを利用すると、ジェネリクス型Tを特定の型に限定することができる。
今回の例だと以下の様に記述する。

function changeBackgroundColor<T extends HTMLElement>(element: T) {
  element.style.backgroundColor = 'red'
  return element
}

extendsキーワードはインターフェースに対しても使う。

interface ValueObject<T> {
  value: T;
  toString(): string;
}

class UserId implements ValueObject<number> {
  publick value: number;

  publick constructor(value: number) {
    this.value = value
  }

  public toString(): string {
    return `${this.value}`
  }
}

class Entity<ID extends ValueObject<unknown>> {
  private id: ID;

  public constructor(id: ID) {
    this.id = id;
  }
}

上記のEntityクラスはValueObjectインターフェースを実装しているクラスをIDとして受ける形。
型引数の制約はimplementsではなくextends

Conditional Types

要約:型の条件分岐。 T extends U ? X : Y と記述する。

type IsString<T> = T extends string ? true : false;
const a: IsString<'a'> = true

extendsの使い方を理解した。
Ifも解けた。

type If<C extends boolean, T, F> = C extends true ? T : F;
high-ghigh-g

Pick

https://github.com/type-challenges/type-challenges/blob/main/questions/00004-easy-pick/README.ja.md

もう一度 Pickに挑戦。
Mapped Typesを理解する。

Mapped Types

要約:主にユニオン型とinを組み合わせて、キー部分を定義したオブジェクトの型

type Language = 'en' | 'fr' | 'it' | 'es'

type Butterfly = {
  [key in Language]: string;
}

consut butterfly: Butterfly = {
  en: "Butterfly",
  fr: "Papillon",
  it: "Farfalla",
  es: "Mariposa",
}

Mapped Typesにはプロパティが追加できない

type Language = 'en' | 'fr' | 'it' | 'es'

type Butterfly = {
  [key in Language]: string;

  // ↓エラー
  name: string
}

上記の様なオブジェクト型を定義したい場合は、別々で定義し、インターセクション型で定義する

type KeyValues = {
  [key in Language]: string;
}
type Name = {
  name: string
}
type KeyValuesAndName = KeyValues & Name

TypeScriptでTruthyを書く場合、予めジェネリクスの入力側(?)をextendsで絞る

type If<C, T, F> = C extends true ? T : F;

type If<C extends boolean, T, F> = C extends true ? T : F;

typescript @ts-expect-error ってなに?

直後の行が型エラーの場合、その型エラーをなくす
つまり、正常な行の直前に@ts-expect-errorを記述すると、逆に型エラーが表示される

Mapped Typesと型制約をつかえばいいはずだが、わからん。

https://dev.classmethod.jp/articles/type-challenges-easy-try/

を参考にすると、以下の記事が紹介されているので読む。
https://qiita.com/ryo2132/items/ce9e13899e45dcfaff9b#mapped-type-での利用

なるほど自力で解けた。

type MyPick<T, K extends keyof T> = {
  [key in K]: T[key] 
}

考え方は、

  • ジェネクスKに型制約をつける
  • Kはunion型だから、それをMapped Typesで書ける
  • keyにはunion型をバラした値が入ってくるからそれを利用してT[key]でTで指定されている型が取得できる
high-ghigh-g

Readonly

https://github.com/type-challenges/type-challenges/blob/main/questions/00007-easy-readonly/README.md

また詰まった。
ユーティリティ型のReadOnlyはObject.freeze的な動きをする。
のは分かる。

それをTSでどう表現するか

https://qiita.com/wataru86/items/f1356b5d1aef30012a20

プロパティに対してreadonly修飾子をつけることで、readonlyにできるよう。

let obj: {
  readonly foo: number;
};

https://typescriptbook.jp/reference/values-types-variables/object/readonly-property

これとMapped Typesを合わせればよかった。

high-ghigh-g

Tuple to Object

https://github.com/type-challenges/type-challenges/blob/main/questions/00011-easy-tuple-to-object/README.ja.md

https://qiita.com/momosetkn/items/0874049f8a2af31c26cd

https://zenn.dev/qnighy/articles/dde3d980b5e386

Mapped Typesでkeyof, typeofを使う方向だとは思う。

typeofはconstで定義された実体の値を型化するものなので、この場合は違う気がしてきた。

array型は、T[number]と記述することで値を型として抽出できる。裏挙動すぎる。
あとはMapped Typesを使う。

解説があった
https://zenn.dev/luvmini511/articles/d89b3ad241e544

index signatureというものがあり、indexを指定することで、型を直接取得することができる。

type person = {
  'name': string
  'age': 10
}

type a = person['name'] // string
type b = person['age'] // 10 (numberではない) 

同じ感覚で、型そのものを指定すると、それに一致する形を丸ごと取得できる。
T[number]としていすれば、array型のindexはnumber型のため、
number型に一致するものをunion型として取得できる。

numeric index signatureと呼ぶ。

TypeScript のグローバル型です。string、symbol、または のいずれかになりますnumber。
https://www.totaltypescript.com/concepts/propertykey-type

type TupleToObject<T extends readonly any[]> = {
  [P in T[number]] : P
}
high-ghigh-g

First of Array

https://github.com/type-challenges/type-challenges/blob/main/questions/00014-easy-first/README.md

index signatureを利用するのは分かってる。

↓これをどう通過するか。
Expect<Equal<First<[]>, never>>,

こんなの書いたけどとおらない。

type First<T extends ((number | object | undefined)[] | never)> = T extends never ? never : T[0] 

違う。neverは空配列の場合の型だ。
だからジェネリクス制約に書かなくても自然と発生するんだ。

いけた

type First<T extends ((number | object | undefined)[])> = T extends never[] ? never : T[0] 

never型を理解する

https://typescriptbook.jp/reference/statements/never

never型は「値を持たない」を意味する。

特性

  • never型の変数へは何も代入できない
  • ただし、never型の値は代入できる(無理やりasでアサーションして)
  • 逆にnever型の値はどんな型の変数にでも代入できる
  • 例外の場合の戻り値の型もnever
  • type NumberString = number & string; はneverになる。インターセクション型は作れない。
  • void型との違いは、void型はundefinedが代入できるが、neverはできない。
  • 関数の戻り値にneverを指定するとエラーになる

網羅性チェックに応用ができる。
defaultを入れるとTSが代入エラーを警告するようになる。

function printLang(ext: Extension): void {
  switch (ext) {
    case "js":
      console.log("JavaScript");
      break;
    case "ts":
      console.log("TypeScript");
      break;
    default:
     // Type 'string' is not assignable to type 'never'.
      const exhaustivenessCheck: never = ext;
      break;
  }
}

みんなこうやってた。。
ジェネリクス制約は毎回書かなくていいのか。なるほど。
あとinferを使ってるコードもあった。

type First<T extends any[]> = T extends [] ? never : T[0]

あとinferを使ってるコードもあった。

type First<T extends any[]> = T extends [infer P, ...any[]] ? P : never

せっかくだから調べてみよう。

https://typescriptbook.jp/reference/type-reuse/infer
https://qiita.com/ehika/items/8f41d4a3c8f9df4af9c3
https://zenn.dev/brachio_takumi/articles/464106a6a80eca8ab919

Conditional Types(extendsを使った三項演算子のようなもの)の右辺でのみ利用できる演算子らしい。

Widening Literal Types, NonWodening Literal Types初めて知った。

const hoge = "HOGE";
// type hoge: "HOGE"
let sameHoge = hoge;
// type sameHoge: string

as const (constアサーション)すると、変数を別の変数に代入してもNonWidening Literal Typesのままで利用ができる。

infer
直訳すると、「推論」
型を割り出すことができる。

↓Tがidというプロパティを持っている場合、そのidの型を返し、なければneverを返す

type Id<T> = T extends { id: infer U } ? U : never 

上だと、わかりにくいと感じたのでもう少し掘り下げ。

下記の場合、Tが[K in keyof T](ユニオンをMappedしたもの)プロパティを持っている場合、そのオブジェクト型の値部分を型として返す。Conditional Typesの条件に当てはまらなければ、never型 が返る。

export type Unpacked<T> = T extends { [K in keyof T]: infer U } ? U : never;

なるほど、元々の問題を見直してみると、以下のように書いていたけど、
推論が怪しく感じる。

type First<T extends any[])> = T extends never[] ? never : T[0] 

こっちのほうがより広い条件に当てはまった書き方に思える。

type First<T extends any[]> = T extends [infer P, ...any[]] ? P : never

型定義のスプレッドに慣れていないので一応見る。
https://zenn.dev/cookiegg/articles/typescript-spread-type

↓このユニオンarray型だと型ガードしにくい
type SpecificArray = (string|number)[];

明確にこう定義したほうが型ガードしやすいが汎用性がまったくない
type SpecificArray = [string, number, number]

上記は、↓で書くと先頭がstringであとがnumberといった書き方が実現できる。
type ArrayType = [string, ...number[]];

改めて以下を見ると、問題の内容が0番目さへ推論できればよいのだから、こういう書き方になる意味がわかった。

type First<T extends any[]> = T extends [infer P, ...any[]] ? P : never
high-ghigh-g

Length of Tuple

https://github.com/type-challenges/type-challenges/blob/main/questions/00018-easy-tuple-length/README.md

as constがついていたら readonly をつけることを忘れない。
楽勝過ぎた。
type Length<T extends readonly any[]> = T['length']

が、
TS的に書くならneverも考慮した方が良い気がしてきた。

type-challenges的にはテストコードを満たす挙動ができていたらok
実務的にはneverを考慮するくらいでいいか

high-ghigh-g

Exclude

https://github.com/type-challenges/type-challenges/blob/main/questions/00043-easy-exclude/README.ja.md

  type Result = MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'

type MyExclude<T, U> = any

mapped type, conditional type, typeof, inferの複合に見える

ロジック整理
Tはユニオンなので、そのままループしinの左辺KとUを比較する

むずい。ヒントだけあやかる。

Conditional Typesでいけそうだけど、Distributiveという概念をしらないといかんらしい。

https://zenn.dev/hrkmtsmt/articles/be9a20fa7d3aaf

ここで言う「分配」はユニオン型の事を指している

ユニオン型の場合の Conditinal Typesは自動でループぽい振る舞いになるみたい。

T extends U ? X : YのTの型引数にA | B | Cが渡された場合T extends U ? X : Yは(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)のように展開されます。

ほー

これはConditional Typesの挙動を理解していないと解けない。

type MyExclude<T, U> = T extends U ? never : T 
high-ghigh-g

Awaited

https://github.com/type-challenges/type-challenges/blob/main/questions/00189-easy-awaited/README.ja.md

Promiseライクな型が内包する型の取得

これも文法の話。

https://zenn.dev/estra/articles/ts-with-promise-type-annotation

type X = Promise<string>
MyAwaited<X> // string

type MyAwaited<T> = Promise<U>

上記を観察
Promise型にジェネリクスでstringが渡されている
Primiseに渡されている

型が型を内包しているように見える

Promise オブジェクトの型はジェネリクスを使って Promise<Type> というような形式
Promiseという関数がstringを返す形になってる

Promise型をもう少し理解

https://typescriptbook.jp/reference/asynchronous/promise

内部挙動の話が多い

多分文法の話なので、ヒントがないと何とも

やっぱinferらしい。

type MyAwaited<T extends Promise<any>> = T extends Promise<infer U> ? U : never

こう書いてみると、プリミティブ型はokっぽい

↓Promiseの入れ子がさばけてない

Promise<Promise<string | number>>

がっつり解説を見よう。
解説みたけど、Promiseの入れ子の解決が書いていない。

再帰的にinferどうするか
他の解答を見て参考にしてみる。

PromiseLikeという型があるのは初見殺しすぎる。

なるほど、再度inferしたあとにconditional typeを挟んで、PromiseLikeに当てはまってたら、自身の型定義を指定すればいいのか

type MyAwaited<T extends PromiseLike<any>> = T extends PromiseLike<infer U>
  ? U extends PromiseLike<any>
    ? MyAwaited<U>
    : U
  : never;

再帰まで気づいてるんだから、自分で自分を指定するところまで考えが及べばよかった。

type ◯◯の部分は関数のように再帰的に指定ができる学び

high-ghigh-g

Concat

https://github.com/type-challenges/type-challenges/blob/main/questions/00533-easy-concat/README.ja.md

type Result = Concat<[1], [2]>; // expected to be [1, 2]

型そのものが[1, 2]

一旦違うのは分かってるけど、以下の様な感じを考えた
numeric index signature

type Concat<T extends any[], U extends any[]> = T[number] & U[number]

解説では、Variadic Tuple Typesを学習する必要があるとのこと
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#variadic-tuple-types

array型も普通の配列と同じ感覚でスプレッド演算子が使えるんだった。

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

https://qiita.com/uhyo/items/7e31bbd93a80ce9cec84

high-ghigh-g

Includes

https://github.com/type-challenges/type-challenges/blob/main/questions/00898-easy-includes/README.ja.md

type isPillarMen = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> // expected to be `false`

必要そうな知識
Conditional Type
配列の値取得の為のnumeric index signature

雰囲気こんなん

 T extends U ? true : false

課題Ifに近い要素がある
配列の値をループする → numeric index signatureでユニオン型にする
ユニオンをmapped types S in T[number]する感じ

多分、裏仕様がありそう

ヒントにあやかる

思ったよりも必要な知識が多かった

Variadic Tuple Types
infer
Conditional Types
Recursive Conditional Types

Variadic Tuple Typesは昨日やったarray型をスプレッド演算子展開するやつ

Recursive Conditional Typesもやったことある気がする
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#recursive-conditional-types

再帰か、これ正式にリカーシブコンディショナルタイプっていうのか

Variadic Tuple Typesはもうちょい深める
https://qiita.com/uhyo/items/7e31bbd93a80ce9cec84

タプル型の中に...Tと書ける機能
あるタプル型の要素たちを別のタプル型に埋め込むことができます。

解説1行目を見た。
Conditional Typeがまだ直感的に理解できていないのかも。
A extends B はあくまでAが主体
U extends T[number] はinの中でifをかけていくイメージ

type Includes<T extends readonly any[], U> = U extends T[number] ? true : false;

falseとなるパターンでtrueとなってしまっているパターンがある。

自力で解けそうにないから解答を見たらわからんかった

type Includes<T extends readonly any[], U> = T extends [infer L, ...infer R]
  ? [U, L] extends [L, U]
    ? true
    : Includes<R, U>
  : false

日本語化する
Tがreadolyのタプル型
チェック対象がU
Tの先頭がL、それ以外がRと推論できて、
かつ先頭Lと末尾Uのリテラルarray型を作って、
先頭と末尾をひっくり返しても同じならtrue
そうでないなら再帰で再度Includesを実行
そのときにRをジェネリクスの第一引数にしている為、先頭が省かれたタプル型になる
Uはそのまま継続
あとは同じ条件で末尾まで行く

[infer L, ...infer R]が満たせないつまり、要素数が1になったらfalse

むず

type Includes<T extends readonly any[], U> =
  T extends [infer L, ...infer R] ?
    [L, U] extends [U, L] ?
      true : Includes<R, U> :
    false;

が無理だったので、回答例を見てたら独自のEqualジェネリクス型を組んでチェックしてる人がいた。

type MyEqual<X, Y> = (<T>() => T extends X ? 1 : 2 ) extends (<T>() => T extends Y ? 1 : 2) ? true : false

type Includes<T extends readonly any[], U> = true extends {
  [I in keyof T]: MyEqual<T[I], U>
}[number] ? true : false

true extends { mapped } ができるんか

右辺でtypeに代入してるはずなのに唐突にアロー関数が出てきて頭がバグってる

(<T>() => T extends X ? 1 : 2 ) extends (<T>() => T extends Y ? 1 : 2) ? true : false

ChatGPTに聞いてみたらかなり高度な型定義らしい

高階型(Higher-Order Types)
<T>() => T extends X ? 1 : 2 は型 T を引数として受け取り、T が X に割り当て可能な場合は 1 を、そうでない場合は 2 を返す関数の型を表します。この高階型は、型の割り当て可能性を評価するために使われます。

ジェネリック型の拡張性(Generics and Extensibility)
<T>() => T extends X ? 1 : 2 のような型を使うことで、任意の型 T が X に対してどのように振る舞うかをテストできます。これを Y にも同様に適用し、これら二つの関数型が等価であるかどうかをチェックします。

やりたいことは、
type MyEqual<X, Y> = (<T>() => T extends X ? 1 : 2 ) extends (<T>() => T extends Y ? 1 : 2) ? true : false

のXとYが等価かどうかの判定らしい

high-ghigh-g

Parameters

https://github.com/type-challenges/type-challenges/blob/main/questions/03312-easy-parameters/README.ja.md

ついにeasy最終課題

arrayの引数をmappedで使うんだと思う。
引数に対するアプローチがわからん。

引数の型だけを抜き出せばいいからmappedとも少しちがう気がする。

function foo(arg1: string, arg2: number): void {}
// ↓
[string, number]

引数に対する構文の知識がいる

雰囲気以下のような感じ。
引数部分をinferとして

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

関数のinferの書き方を学ぶ必要があるとのこと

少し修正したらベースの書き方はいけた。

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

あとは、Uを加工する。
少し近づいてる気がする。
inferで取得したUをconditional typesでタプル型に絞り込んで、
numeric index signatureを書けば取得できると思ったら違うらしい。

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

解答を見たら、ちょっと思っていたこととずれていた

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

関数の型を再現する場合、引数のスプレッド演算子の部分も忠実に書かないといけない
ジェネリクス制約が T extends (...args: any[]) => any こう書かれていたら、
inferで書く際もT extends (...args: infer U) => any と書かないといけない。

引数はデフォルトでタプル型になっているため、関数の型は忠実に書くという知識だけ知っていれば、conditional typesで取得するまではできた。

複数引数がある関数の型をざっくりと書くと
(...args: any[]) => any になるのを理解

high-ghigh-g

easy復習

Exclude

Distributive Conditional Types がまだ理解できていない。

type ConditionalTypes<T, U> = T extends U ? X : Y;
// Tが a | b となるのが、Distributive Conditional Types

Distributiveは分配という意味
'a' | 'b' extends U ? X : Y の場合、'a' extends U ? X : Y'b' extends U ? X : Y が自動で展開される。

上記を利用して、以下のようにConditional Typesを書くと、当てはまらない型の場合にneverとなるため、
ユーティリティ型のExcludeが再現できる。

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

Awaited

なんとなくinfer と 再帰条件型は覚えてた。
再帰で型を特定する場合、都度ジェネリクス制約を挟む必要がある。

↓こう書いた時、

type MyAwaited<T extends Promise<any>> =
  T extends Promise<infer U>
    ? U extends Promise<any>
      ?
        MyAwaited<U> : U
    : never;

↓このパターンのテストが満たせていない

type T = { then: (onfulfilled: (arg: number) => any) => any }

Expect<Equal<MyAwaited<T>, number>>

PromiseLike<T>インターフェイスがビルトインで存在するとのこと
https://qiita.com/suin/items/b9d00dff380486338ecd

↓これでいけた

type MyAwaited<T extends PromiseLike<any>> =
  T extends PromiseLike<infer U>
    ? U extends PromiseLike<any>
      ?
        MyAwaited<U> : U
    : never;

If

Conditional Types(条件型)

Concat

Variadic Tuple Types(可変長タプル型)

Includes

クソむずかった記憶

inferを直感的に利用できないときに配列を分解する方法に慣れてない。

ジェネリクスを分解する際にとりあえず T extends [inferU] ? (ここで条件型をかます) を覚えておく。

↓この形は理解

type Includes<T extends readonly any[], U> =
  T extends [infer L,  ...infer R]
    ? [L, U] extends [U, L]
      ? true : Includes<R, U>
    : false

↓このテストが満たせていない

 Expect<Equal<Includes<[{ a: 'A' }], { readonly a: 'A' }>, false>>,
  Expect<Equal<Includes<[{ readonly a: 'A' }], { a: 'A' }>, false>>,

type-challenges側で用意されたUtilを使ってる人もいたが本質ではない。

type Includes<T extends readonly any[], U> =
  T extends [infer L,  ...infer R]
    ? Equal<U, L> extends true
      ? true : Includes<R, U>
    : false

これっぽいのを利用してる人が多い

type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y
  ? 1
  : 2
  ? true
  : false;

Push

Unshift

どちらも Variadic Tuple Type

Parameters

inferで解決
関数の場合は、しっかり全部書く

T extends (...args: infer U) => any ? U : []
high-ghigh-g

Omit

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

Conditional Typeの分配を使うと思ってる。
これまでと違うのはTがarrayではなくオブジェクト

ちがう。mapped・・・の逆だ

mapped → index → 存在しないやつをinferで作る?

正解ではないけど、文法的には書ける。

type MyOmit<T, K extends keyof T> = {
  [key in K]: T[key] extends never ? never : T[key]
}

ヒントを見る

Key Remapping in Mapped Typesという知識が必要らしい
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#key-remapping-in-mapped-types

Mapped Typesを利用して元のプロパティから新しいプロパティを生成したり、あるプロパティを除外する為にはas句を使用する
https://zenn.dev/link/comments/618ef3139c93c0

Capitalizeというビルトインの型がTS4.1からあるらしい
そして、その型定義でinstrinsicというワードが出てきた。

type Capitalize<S extends string> = intrinsic;

全部書いてた
https://zenn.dev/uhyo/articles/typescript-intrinsic

これは実装がコンパイラの内部実装として隠蔽されていることを意味しています。言い換えれば、TypeScriptの通常の型定義では表現できないような特殊な機能を表現するものです。
intrinsicキーワードの必要性
TypeScript 4.1では文字列の大文字・小文字変換の機能を実装することになりましたが、実は当初の実装ではintrinsicを使っていませんでした。代わりに、次のようにuppercase・lowercase・capitalize・uncapitalizeという4つのキーワードによる新たな構文を定義され、それらを用いる方式となっていました

話を戻してas句の2つの使い方

  • template literal types(リテラル型 + テンプレートリテラル)と組み合わせて利用するリネーム
  • as句の中でneverを返した場合に、プロパティ除外ができる

今回の課題は2つ目の使い方

課題に関して

type MyOmit<T, K extends keyof T> = {
  [key in keyof T as key extends K ? never : key]: T[key]
}

keyがユニオン型に含まれているかどうかをConditional Typesで判断

key extends K ? never : key

T as ◯ で◯がneverなら、for文ないでcontinueされるイメージ

他の解答でasを使わない別解があった。
カスタムでExclude型を作成し、Mappedを行っている。
as句でやっているイメージはこれ

type MyExclude<A, B> = A extends B ? never : A;
type MyOmit<T, K extends keyof T> = { [S in MyExclude<keyof T, K>]: T[S] }

https://qiita.com/ryokkkke/items/7b16c238377b3b57d77f

high-ghigh-g

Readonly 2

https://github.com/type-challenges/type-challenges/blob/main/questions/00008-medium-readonly-2/README.ja.md

困りごと
2 argument typeのときの絞り方
readonlyの場合分けの書き方

ヒントを見た

インターセクション型を使うらしい(&で繋ぐやつ)
https://www.typescriptlang.org/docs/handbook/2/objects.html#intersection-types

イメージ
asを利用して、一致する場合、一致しない場合を書いてインターセクション型(&)でガッチャンコ

大枠はこれでいけた

type MyReadonly2<T, K extends keyof T> = {
  readonly [P in keyof T as P extends K ? P : never]: T[P]
} & {
  [P in keyof T as P extends K ? never : P]: T[P]
}

あとは2 argument typeのときの絞り方
答えを見たら、オプション引数(?)的なので絞ると思っていたら、初期値を代入する形で対応していた。

ジェネリクスでも=を使った代入が利用できる。

type MyReadonly2<T, K extends keyof T = keyof T> = {
  readonly [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に該当するものすべてにreadonlyを付与し、それ以外は通常の型にするイメージ

type MyReadonly2<T, K extends keyof T = keyof T> = {
  readonly [P in K]:T[P]
} & {
  [P in keyof T as P extends K ? never : P] : T[P]
}
high-ghigh-g

Deep Readonly

https://github.com/type-challenges/type-challenges/blob/main/questions/00009-medium-deep-readonly/README.ja.md

Mapped Typesを2段階でかける。

Mapped利用メモ
オブジェクト → keyof
ユニオン → そのまま

Mappedの場合もユニオン型には勝手に分配が適用される

なんとなくこんな雰囲気のやつを考えた。
オブジェクトの入れ子になってる部分が課題

type DeepReadonly<T> = {
  readonly [key in keyof T]: T[key] extends PropertyKey ? T[key]: DeepReadonly<T>
}

ヒントを見る
必要な知識は以下。大体以下は考慮できていた。
Mapped Types
Indexed Access Types
Conditional Types
Recursive Conditional Types

つまり T[P] がオブジェクトならさらにサブオブジェクトまで readonly とし、それ以外ならそのまま T[P] を返せばよい

これも把握している。ただ、オブジェクトならの条件の書き方が分からない。
PropertyKeyではないみたい。

またでたオブジェクトの判定は Record というビルトイン型を利用する
https://typescriptbook.jp/reference/type-reuse/utility-types/record

Record<Keys, Type> → プロパティがKeys(Keysはユニオン型)で、その値の型がTypeのオブジェクトを作るユーティリティ型

値がオブジェクト型かどうかは Record<string, unknown> で判定する

type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends Record<string, unknown> ? DeepReadonly<T[P]> : T[P];
}

ただし、これでもエラーになっている。
解答を見てみる。
extends Function でFunction型の判定条件を追加している。
というか Record<string, unknown> の知識がなくても { [k in string]: any } でいいんだ。

そもそもRecordやオブジェクト判定を利用しない人がいた。
keyofがneverならオブジェクト以外という考え方(最初のProparyKeyの考え方に近い)

type DeepReadonly<T> = {
  readonly [P in keyof T]: keyof T[P] extends never ? T[P] : DeepReadonly<T[P]>
}

こっちと何が違うか

  readonly [P in keyof T]: T[P] extends Record<string, unknown> ? DeepReadonly<T[P]> : T[P]

これだと、以下の部分の展開がうまくできていない。

      l: [
        'hi',
        {
          m: ['hey']
        },
      ]

keyofをカマスと自然と配列の展開が行われる

high-ghigh-g

Tuple to Union

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

パット見簡単に見える。

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

やっぱりインデックス型で一発だった。

他の回答例

ジェネリクス制約を利用せず、Conditional Typeでinferを利用している

export type TupleToUnion<T> = T extends Array<infer ITEMS> ? ITEMS : never

ArrayLike型を使っている。
なぜかF、lastの形でinferを使っている

type TupleToUnion<T extends ArrayLike<any>> = T extends [infer F, ...infer Last] ? TupleToUnion<Last> | F : never
high-ghigh-g

Chainable Options

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

チェイン可能なオプション
また取っ掛かりのわからないやつきた。

type Chainable = {
  option(key: string, value: any): any
  get(): any
}

他の方々の解答を早速覗いてみる。

記述のポイントは、以下だと感じた。

  • ジェネリクス制約を書く場所を意識
    • 型全体のジェネリクス+初期値
    • メソッドに対するジェネリクス
    • 再帰

この辺りからeasyの時よりも各々の書き方の個性が出てきているように思う。

解決方法が思いつかないときは最小限の解から考えてみる。

この人の思考の流れがいい。
https://github.com/type-challenges/type-challenges/issues/15337

まずはget()だけに着目し、空オブジェクトを返すようにする。

type Chainable<Props = {}> = {
  option(key: string, value: any): any
  get(): Props
}

次に option部分を調整。再帰を記述する。

type Chainable<Props = {}> = {
  option(key: string, value: any): Chainable
  get(): Props
}

Chainableはジェネリクスの形を取る為、
Chainable<◯◯>と記述する必要がある。

◯◯の部分にオブジェクトの形式をいれる必要はあるが、
key, valueの型をそのまま利用したいため、ジェネリクス制約を利用する

type Chainable<Props = {}> = {
  option<K extends string, T>(key: K, value: T): Chainable<Props & { [key in K]: T }>;
  get(): Props
}

これだと、以下のような、プロパティが同じ場合に、上書きするようなパターンが解決出来ない

// name: numberとなってほしいが、なってくれない
const result3 = a
  .option('name', 'another name')
  // @ts-expect-error
  .option('name', 123)
  .get()

Record(ユーティリティ型)を利用している人がいる
https://typescriptbook.jp/reference/type-reuse/utility-types/record

Record<Keys, Types>
キーと値の型を指定することで以下のような型が作れる。

type StringNumber = Record<string, number>;
const value: StringNumber = { a: 1, b: 2, c: 3 };

想像以上にむずい。
いくつかissueを見てみる。

Omitを利用している人がいる
https://typescriptbook.jp/reference/type-reuse/utility-types/omit

Omit<T, Keys>
オブジェクト型Tの中からKeysで指定したプロパティを除く

type User = {
  surname: string;
  middleName?: string;
  givenName: string;
  age: number;
  address?: string;
  nationality: string;
  createdAt: string;
  updatedAt: string;
};
type Optional = "age" | "address" | "nationality" | "createdAt" | "updatedAt";
type Person = Omit<User, Optional>;

// res
type Person = {
  surname: string;
  middleName?: string;
  givenName: string;
};

Exclude
https://typescriptbook.jp/reference/type-reuse/utility-types/exclude

最終解

type Chainable<T = {}> = {
  option<K extends string, V>(key: Exclude<K, keyof T>, value: V): Chainable<Omit<T,K> & Record<K, V>>;
  get(): T;
};

Tで指定されている型よりもKを優先する場合、Excludeで絞って、さらにOmitかあ。
これはむずい。

Distributive Conditional Type (分配的条件型)の備忘

// Exclude的実装
type Hoge<T, U extends T> = T extends U ? never : T;

// Hogeにunion型の型引数を渡す場合
type Fuga = Hoge<'a' | 'b' | 'c' | 'd', 'a | b'>

// Conditional Typeのextendsの右側のunionが順番に展開されていく
/*
('a' extends 'a | b' ? never : T) |
('b' extends 'a | b' ? never : T) |
('c' extends 'a | b' ? never : T) |
('d' extends 'a | b' ? never : T) |
*/

Key Remapping の備忘

Mapped Typesのキー部分の記述でasを利用する方法。
型アサーションのasとは別物。

↓の様に記述することで、オブジェクト型のキー部分をリネームできる。

type Obj = { [P in Key as `remapped-${P}`]: string };

ちなみにasでneverを指定するとキー自体が生成されなくなる。
キー部分でConditional Typeと併用することで、キーの表示・非表示切り替えが書ける。

type-challenges MyOmitより

type MyOmit<T extends {[S: string]: any}, K extends keyof T> = {
  [key in keyof T as key extends K ? never : key]:T[key]
}

https://qiita.com/ryokkkke/items/7b16c238377b3b57d77f

type-challengesのmiddleからユーティリティ型を理解しておいたほうがスマートに問題に取り組めるように思えてきた。
https://zenn.dev/tris/articles/ts-utility-types
https://www.typescriptlang.org/docs/handbook/utility-types.html

high-ghigh-g

Pop

https://github.com/type-challenges/type-challenges/blob/main/questions/00016-medium-pop/README.ja.md

この課題も Last of Array と基本の考え方は似てそう。
自分の解

type Pop<T extends any[]> = T extends [...infer U, infer L] ? U : [] 

回答を見る感じ、似たような解がある。

↓こちらの方が無駄なinferの記述がなくて良さそう。

type Pop<T extends any[]> = T extends [...infer U, any] ? U : [] 

↓ちょっとだけ煩雑な気はするけど、lengthを見て空配列を返すかどうかの判定をしてる人がいた

type Pop<T extends any[]> = T['length'] extends 0  ? [] : T extends [...infer Arg, infer L] ? Arg : never
high-ghigh-g

Promise.all

https://github.com/type-challenges/type-challenges/blob/main/questions/00020-medium-promise-all/README.ja.md

Promiseに対しての型付けは結構苦手。
以下のようにPromiseを戻り値としてもつ変数を配列に格納し、それをPromiseAllに渡している。

  const promise1 = Promise.resolve(3);
  const promise2 = 42;
  const promise3 = new Promise<string>((resolve, reject) => {
    setTimeout(resolve, 100, 'foo');
  });

  const p = PromiseAll([promise1, promise2, promise3] as const)

難しいと思ったけど、問題文を読んでるとできそうな気がしてきた。

まずシンプルに書いてみたもの。

declare function PromiseAll<T extends any[]>(values: T): Promise<T>

以下のテストが通ってない。Prmise.resolve()の場合。

const promiseAllTest2 = PromiseAll([1, 2, Promise.resolve(3)] as const)
const promiseAllTest3 = PromiseAll([1, 2, Promise.resolve(3)])
const promiseAllTest4 = PromiseAll<Array<number | Promise<number>>>([1, 2, 3])

Expect<Equal<typeof promiseAllTest2, Promise<[1, 2, number]>>>,
Expect<Equal<typeof promiseAllTest3, Promise<[number, number, number]>>>,
Expect<Equal<typeof promiseAllTest4, Promise<number[]>>>,

一旦、Promise.resolveをなんとかすればいいのか

declare function PromiseAll<T extends any[]>(values: T):
  Promise<T extends Promise<infer U> ? U : T>

変わらずエラーPromise.resolveの戻り値の型がなにか調べてみる。
↓こんな感じみたい。

    /**
     * Creates a new resolved promise for the provided value.
     * @param value A promise.
     * @returns A promise whose internal state matches the provided promise.
     */
    resolve<T>(value: T | PromiseLike<T>): Promise<T>;

よくよく見たら配列で渡されてるからインデックスシグネチャとかでユニオン型にしないといけないかも。
わからん。。解答をいくつか見てみる。

シンプルな解答があった。

declare function PromiseAll<T extends any[]>(values: readonly [...T]): Promise<{
  [key in keyof T]: Awaited<T[key]>
}>
  • 関数の引数valuesに対して readonly [...T] で配列の要素を展開しつつreadonlyにしている
  • Mapped Type + Awaited でPromiseを返している

Awaitedとは
Promiseを再帰的に回してラップを解くユーティリティ関数とのこと
https://www.typescriptlang.org/docs/handbook/utility-types.html

今回の課題はAwaitedの存在を理解していたら解までの道筋がシンプルになっていた。

Awaitedを利用せずにConditional TypeでPromiseのラップを解きながら対応しているものもあった

declare function PromiseAll<T extends any[]>(values: readonly [...T]): Promise<{
  [K in keyof T]: T[K] extends Promise<infer R> ? R : T[K] extends number ? T[K] : number
}>

readonly [...T] 配列の値に対してはこの表現も重要に感じる。
TS上の配列の扱いは、キーが数値のオブジェクト型としてとらえたら良さげ。

high-ghigh-g

Type Lookup

https://github.com/type-challenges/type-challenges/blob/main/questions/00062-medium-type-lookup/README.ja.md

簡単そうで取っ掛かりが見えない。
感覚的に書くと以下。これだと通らない。

type LookUp<U, T> = U['type'] extends T ? U : never

型制約をつけたいけど、どうやったらいいかわからない。
やりたいことは、Uの特徴の制約
Uはtypeを持つinterfaceのユニオン

解答を見る。
めちゃくちゃシンプルなやり方でやってる人がいた。

type LookUp<U, T> = U extends {type: T} ? U : never;

これが良さそう。

type LookUp<U, T extends string> = {
  [K in T]: U extends { type: T } ? U : never
}[T]

Mapped TypeでTをキーに持つオブジェクト型をつくりつつ、最後にインデックスシグネチャで対象のUを取り出す形。
typeを持つオブジェクトを表したい場合、Conditional Typeで extends { type T} で表せばいいみたい。

high-ghigh-g

Trim Left

https://github.com/type-challenges/type-challenges/blob/main/questions/00106-medium-trimleft/README.ja.md

Conditional Types + infer + 文字列リテラル と推測
直感的に書く。無理

type TrimLeft<S extends string> = S extends infer ' ' + T ? T : S

テンプレートリテラル型にも使える
https://scrapbox.io/mkizka/Template_Literal_Typesとinferの理解

inferを使って再帰

type TrimLeft<S extends string> = S extends ` ${infer T}` ? TrimLeft<T> : S

以下のテストパターンが通ってない。

  Expect<Equal<TrimLeft<'   \n\t foo bar '>, 'foo bar '>>,
  Expect<Equal<TrimLeft<' \n\t'>, ''>>,

どっちにも対応出来るようにtypeを変数的に生成

type P = ' ' | '\n\t'
type TrimLeft<S extends string> = S extends `${P}${infer T}` ? TrimLeft<T> : S

解答を見たら、汎用的なものがあった。

type Whitespace = '\n' | ' ' | '\t';
type TrimLeft<S> = S extends `${Whitespace}${infer U}` ? TrimLeft<U> : S;
high-ghigh-g

Trim

https://github.com/type-challenges/type-challenges/blob/main/questions/00108-medium-trim/README.ja.md

Trim LeftにTrim Rightライクなものを追加すればokな認識。
出来た。

type Space = ' ' | '\t' | '\n'

type Trim<S extends string> =
  S extends `${Space}${infer U}`
  ? Trim<U>
  : S extends `${infer O}${Space}`
    ? Trim<O>
    : S

他解答も見てみる。

Conditional Typesの右辺をユニオンで指定すれば、
多段階のConditional Typesを利用しなくても良かった

type Space = ' ' | '\t' | '\n';
type Trim<S extends string> = S extends `${Space}${infer T}` | `${infer T}${Space}` ? Trim<T> : S;

元々やろうとしてたことをやってる人がいた。
Genericsの中に別のtypeを入れ子にすればよかった。

type WhiteSpace = ' ' | '\n' | '\t';
type TrimLeft<S extends string> = S extends `${WhiteSpace}${infer T}` ? TrimLeft<T> : S;
type TrimRight<S extends string> = S extends `${infer T}${WhiteSpace}` ? TrimRight<T> : S;
type Trim<S extends string> = TrimRight<TrimLeft<S>>;
high-ghigh-g

Capitalize

https://github.com/type-challenges/type-challenges/blob/main/questions/00110-medium-capitalize/README.ja.md

大文字小文字の取得ってどうやればいいんだろう。
取っ掛かりがわからない。

upper的なユーティリティ型があるかどうか。
→あったUppercase、Lowercaseもあった

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

リテラル型 + inferで最初の文字とそれ以外という区別の仕方
https://zenn.dev/okunokentaro/articles/01gmpkp9gzb1zf217mjb820yrw

単純に以下のように書いた場合、C1には先頭の1文字目が、C2にはそれ移行の文字が入るみたい

`${infer C1}${infer C2}`

出来た。

type MyCapitalize<S extends string> = S extends `${infer Head}${infer Other}` ? `${Uppercase<Head>}${Other}` : ''

他の解答も見てみる。

猛者がいた。
ユーティリティ型を使わない強い意志を感じる。

interface ToUpperCase {
    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"
}

type LowerCase = keyof ToUpperCase
type MyCapitalize<S extends string> = S extends `${infer First extends LowerCase}${infer Rest}` ? `${ToUpperCase[First]}${Rest}` : S

Conditional Typesの条件から外れる場合、そのままSを返せばよかった。

type Capitalize<S extends string> = S extends `${infer x}${infer tail}` ? `${Uppercase<x>}${tail}` : S;

内部実装気になる
https://zenn.dev/uhyo/articles/typescript-intrinsic

high-ghigh-g

Replace

https://github.com/type-challenges/type-challenges/blob/main/questions/00116-medium-replace/README.ja.md

今回のお題もリテラル型
できた

type Replace<S extends string, From extends string, To extends string> =
  From extends ''
    ? S : S extends`${infer First}${From}${infer Last}`
      ? `${First}${To}${Last}` : S

他の解答をみてみる。

Fromが空文字だった場合に、プレースホルダ内でnever型に変換しているのがあって、賢いと思った。

type Replace<S extends string, From extends string, To extends string> =
  S extends `${infer L}${From extends '' ? never : From}${infer R}`
  ? `${L}${To}${R}`
  : S

こっちはFromが空文字の場合、${U}${From}${V} としている賢い。

type Replace<S extends string, From extends string, To extends string> = S extends `${infer U}${From}${infer V}` ?
  From extends '' ?
  `${U}${From}${V}` :
  `${U}${To}${V}` :
  S;
high-ghigh-g

ReplaceAll

https://github.com/type-challenges/type-challenges/blob/main/questions/00119-medium-replaceall/README.ja.md

Replaceを再帰的にやればいいかと思いきや、通らないテストケースがある。

type ReplaceAll<S extends string, From extends string, To extends string> = From extends ''
    ? S : S extends`${infer First}${From}${infer Last}` ? ReplaceAll<`${First}${To}${Last}`, From, To> : S

再帰的にやると↓のパターンの場合にやりすぎた変換になるのか。

  Expect<Equal<ReplaceAll<'foobarfoobar', 'ob', 'b'>, 'fobarfobar'>>,
  Expect<Equal<ReplaceAll<'foboorfoboar', 'bo', 'b'>, 'foborfobar'>>,

わからん。解答を見る。

なるほど、From以降の部分にだけ再帰をかけるのか。

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

↓この人は、Toで変換した以外の場所を再帰してた。

type ReplaceAll<S extends string, From extends string, To extends string> = From extends '' ? S : S extends `${infer R}${From}${infer Q}` ? `${ReplaceAll<R, From, To>}${To}${ReplaceAll<Q, From, To>}` : S;
high-ghigh-g

Append Argument

https://github.com/type-challenges/type-challenges/blob/main/questions/00191-medium-append-argument/README.ja.md

こんな感じで考えたけど、テストパターンの1つめが通らない。

type AppendArgument<Fn extends (...args: any) => any, A> = Fn extends (...args: infer S) => infer T ? (x: A, ...args: S) => T: never

引数に対するアノテーションむずい。
引数で...を利用する場合、レスト構文といい、複数の引数をまとめる意味をもつ。
このばあい、(...args: S, x: A) => Tのようにするとエラーになる。レスト構文は、引数の最後に書かないといけないため。
レストの記述を展開する様な形にするのかな。
解答見る。

なるほど!!!...argsとxを分けるんじゃなく、...argsの一要素として考えるのか。

type AppendArgument<Fn, A> = Fn extends (...args: infer R) => infer T ? (...args: [...R, A]) => T : never
high-ghigh-g

Permutation

https://github.com/type-challenges/type-challenges/blob/main/questions/00296-medium-permutation/README.ja.md

Distributive Conditional Typesな気がする。
全然取り掛かりが分からん。

解答をみる

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

[never]を使ってる。

↓の部分も思いつかなかった。ジェネリクスの引数の数が一つの場合、考慮する癖をつける。

Permutation<T, U = T>

T extends U の部分で分配ができる。
Exclude<U, T>再帰的にUから分配のTを取り除きつつ、レスト構文で展開してる。

現状、Distributiveの挙動が頭の中でトレースできないからこれ系の課題はむずい。

high-ghigh-g

Length of String

https://github.com/type-challenges/type-challenges/blob/main/questions/00298-medium-length-of-string/README.ja.md

型でカウントアップができる?
インクリメントの機構がないんじゃ?
取り掛かりが分からない。。

解答を見る。

Sをinferで1文字目とそれ以外に分けて、array型を作成後、そのarray型のlengthを見てる。なるほど

type StrToArray<S> = S extends `${infer x}${infer xs}` ? [1, ...StrToArray<xs>] : [];
type LengthOfString<S extends string> = StrToArray<S>['length'];

この人はジェネリクスでとる値を増やして、ワンラインでやってる。

type LengthOfString<
  S extends string,
  T extends string[] = []
> = S extends `${infer F}${infer R}`
  ? LengthOfString<R, [...T, F]>
  : T['length'];

...StrToArray<xs>に関して、レスト構文を使わずに [1, StrToArray<xs>]とした場合、
[1, [1, [1, ...]]]の様な形で展開されてしまう。
なので、配列をフラットに保ちたいときはレスト構文を使う必要がある。

high-ghigh-g

Flatten

https://github.com/type-challenges/type-challenges/blob/main/questions/00459-medium-flatten/README.ja.md

こんな雰囲気のことを書くのかなと思ったら違った。

type Flatten<T extends any[]> = T extends  [infer U] ? Flatten<[U]> : []

解答を見る

配列を処理する場合も文字列を処理する場合と同じように、infer F(先頭)と ...infer R(それ以外)の記法を使うことを理解。
問題を切り分けていきながら対象を小さくしていく感覚。

type Flatten<T> = T extends []
  ? [] 
  : T extends [infer First, ...infer Rest]
    ? [...Flatten<First>, ...Flatten<Rest>]
    : [T]
high-ghigh-g

Append to object

https://github.com/type-challenges/type-challenges/blob/main/questions/00527-medium-append-to-object/README.ja.md

感覚的には、以下のように書きたい。

type AppendToObject<T, U extends string, V> = {
  [K in keyof T]: T[K]
}  & { U: V }

以下も違う。

T & {
  [K in U]: V
}

T[U] = V的なことがしたい。代入の方法
これも初見殺しな感じするので、解答を見る。

なるほど。
keyof T | Uって書けば、文字列をunionに含むことができるんだ。(それはそうか)
あとはConditional TypesでTのキーとしてUが含まれているかを判定し、該当しなければVを表示する形でいいみたい。

type AppendToObject<T, U extends keyof any, V> = {
  [K in keyof T | U]: K extends keyof T ? T[K] : V;
};
high-ghigh-g

Absolute

https://github.com/type-challenges/type-challenges/blob/main/questions/00529-medium-absolute/README.ja.md

string型に対しての対応は以下でできる。

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

問題は数値系の型に対応する方法。
numberをstringにキャストする方法をしらないので解答を見る。

結構シンプルな解答があった。
Conditional Typesの左側でリテラル型で書いてしまえばいいのか。シンプル。

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

キャスト用の型を書くのが正攻法とおもったけど、どのみち上記の書き方の方が、手軽にstringにキャストできているので、上記の書き方の方が良いと思った。

type NumToStr<T extends number | bigint> = `${T}`
high-ghigh-g

String to Union

https://github.com/type-challenges/type-challenges/blob/main/questions/00531-medium-string-to-union/README.ja.md

今までの知識の応用だった為、一瞬で解けた。

type StringToUnion<T extends string> = T extends `${infer F}${infer L}` ? F | StringToUnion<L> : never

一応解答を見る。

たまに見るジェネリクスを増やすパターンについて、初見の発想として浮かぶようにしておきたい。
Rに配列格納していって、最後 numeric index signatureを利用してユニオン表示する形。
賢いやり方だけど、この場合は流石に冗長。

type StringToUnion<T extends string, R extends string[] = []>
  = T extends `${infer First}${infer Rest}`
      ? StringToUnion<Rest, [...R, First]>
      : R[number]
high-ghigh-g

Merge

https://github.com/type-challenges/type-challenges/blob/main/questions/00599-medium-merge/README.ja.md

解けた。
これも以前やった問題の応用で、Mapped Typesを利用するときはキー部分でkeyofをユニオンで記述し、値部分でConditional Typesを記述する形を利用する。

type Merge<F, S> = {
  [K in keyof F | keyof S]:
    K extends keyof S ?
      S[K] :
      K extends keyof F ? F[K] : never
}

解答を見てみる。

ワンラインで賢いやり方をしてる人が居た。
keyof (F & S)とすれば、キー部分の記述も値部分の記述も省略できる。

type Merge<F extends Record<string, any>, S extends Record<string, any>> = {
 [k in keyof (F & S)]: k extends keyof S ? S[k] : (F & S)[k]
}

あと度々、Recordを忘れる。
Record<Keys, Type>
https://typescriptbook.jp/reference/type-reuse/utility-types/record

type StringNumber = Record<string, number>;
const value: StringNumber = { a: 1, b: 2, c: 3 };
type Person = Record<"firstName" | "middleName" | "lastName", string>;
const person: Person = {
  firstName: "Robert",
  middleName: "Cecil",
  lastName: "Martin",
};

これがもっともシンプルな解答に思える。
ジェネリクスを増やして、そこにFとOのインターセクションを初期値として格納。
ジェネリクスを変数定義の場所として扱うこともできるのか。

type Merge<F, S, O = F & S> = { [K in keyof O]: K extends keyof S ? S[K] : O[K] }
high-ghigh-g

KebabCase

https://github.com/type-challenges/type-challenges/blob/main/questions/00612-medium-kebabcase/README.ja.md

以前に大文字に変換するユーティリティ関数があることをやった。
シンプルな問題かなと思いつつ、やりだすと大文字から小文字に変換する際の制約があることに気づく。

一応、大文字から小文字への変換処理は以下でok。

type KebabCase<S extends string> =
  S extends `${infer F}${infer L}` ?
    `${F extends Uppercase<F> ? Lowercase<F> : F}${KebabCase<L>}`
  : ''

あとは-をつける処理。
考慮しないと行けないのは、infer Fの部分が先頭文字だった時以外の場合に、-をつける。
なので、多分ジェネリクスをもう一つ用意し、array型 + numeric index signatureを利用する気がする。

だめだ。array型周りがうまく書けない。
解答を見る。

array型を使わずにシンプルに処理しつつ、Uncapitalizeというユーティリティ型を使っている人が居た。
Uncapitalize → 1文字目を小文字に変換するユーティリティ型

inferで先頭(S1)とそれ以外(S2)に分ける
→S2の先頭を小文字化したものとS2が一致していた場合、素直にS1を小文字化しS2と連結
→S2の先頭を小文字化したものとS2が一致しなかった場合、-を埋め込む

type KebabCase<S extends string> = S extends `${infer S1}${infer S2}`
  ? S2 extends Uncapitalize<S2>
  ? `${Uncapitalize<S1>}${KebabCase<S2>}`
  : `${Uncapitalize<S1>}-${KebabCase<S2>}`
  : S;

基本的にこのやりかたで進めている人が多そう。
先頭かどうかの判断は、lengthを見なくてもinferを使えばできると学習

high-ghigh-g

Diff

https://github.com/type-challenges/type-challenges/blob/main/questions/00645-medium-diff/README.md

Mergeの逆版に思える。
ジェネリクス制約でRecordを使えたのでRecordが理解できている。

雰囲気はこんな感じ。通らないけど・・・。

type Diff<
  O extends Record<string, any>,
  O1 extends Record<string, any>,
  KEY extends number | string | symbol = keyof (O & O1) 
> = {
  [K in KEY]:
    K extends keyof O
    ?
      K extends keyof O1 ? never : O[K]
    :
      K extends keyof O1 ? O1[K] : never
}

これだと、値にneverを付けているだけだ。
Key Remapping in Mapped Typesを利用する。

以下のように書いたけど、通らない。。

type Diff<
  O extends Record<string, any>,
  O1 extends Record<string, any>,
  O2 extends (O & O1),
  KEY extends number | string | symbol = keyof O2
> = {
  [
    K in KEY as K extends keyof O ?
      K extends keyof O1 ? never : O[K]
    :
      K extends keyof O1 ? O1[K] : never
  ]: O2[K]
}

解答を見る。

めちゃくちゃシンプルに書いている人が居た。。
2つのオブジェクトのインターセクションのMapped Typesのキーに対し、
keyof O | O1と一致するものがあればnever一致しなければそのまま採用ってことか。
ユニオンやインターセクションを利用した際のベン図的な条件が弱い。

type Diff<O, O1> = {
  [K in keyof (O & O1) as K extends keyof (O | O1) ? never : K]: (O & O1)[K];
};

Excludeであらかじめkeyof(O | O1)を取り除いたユニオンに、Mapped Typesを利用する形。

type Diff<O, O1> = {
  [K in Exclude<keyof (O & O1), keyof(O | O1)>]: (O & O1)[K]
}

Omitを使うと、以下のような形で表現できる。

type Diff<O, O1> = Omit<O & O1, keyof (O | O1)>;

Omit<T, Keys>は、オブジェクトの型TからKeysで指定したプロパティを除いたobject型を返すユーティリティ型です。

high-ghigh-g

AnyOf

https://github.com/type-challenges/type-challenges/blob/main/questions/00949-medium-anyof/README.md

和訳がないのでメモ

  • array型がジェネリクスに与えられる
  • 要素の一つでも truthy なら true を返す
  • 要素が空または、すべてが falsy なら false を返す

trusy, falsyの判定を行うユーティリティ型はなさそうだし、今回はPythonライクな条件になるため、て実装が必要そう

Python trusy的ユーティリティを作ろう。
以下を一つずつチェックする感じになる
boolean true
number: 0でない
string: 空文字でない
array 空配列ではない
object 空オブジェクトではない
undefined, null,never ではない

上記のユニオンの型をまず書く
falsyな型を書く方が早そう

以下でなければ、true的な

type Falsy = false | 0 | '' | [] | {} | null | undefined | never

安易にNumeric Index Signature + Distributive Conditional Types を利用したら行けると思ったけど、
これだと true | false | true | falseみたいな条件が返ってきそう。

type Falsy = false | 0 | '' | [] | {} | null | undefined
type AnyOf<T extends readonly any[]> = T[number] extends Falsy ? false : true;

根本のor条件的な書き方がわかってなさそう。解答を見る。

なんか同じ様な書き方で通っている人がいた。

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

オブジェクトのfalsyな条件は、{} という記述より {[key: string]: never} の方が良いみたい。
型が {} だと、オブジェクト型全般を許可する意味になる為、 {name: 'test'} も含まれることになる。

方向性は正しくて、空オブジェクトの指定の仕方が正しくない感じだった。おしい。
{[key: string]: never} は学びになった。

high-ghigh-g

IsNever

https://github.com/type-challenges/type-challenges/blob/main/questions/01042-medium-isnever/README.md

Neverの判定について、T extends never だと判定できない。。
この問題のレベルで、この難易度は流石に無いと思ったけど、その通りだった。

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

neverはNaNみたいな存在なのか。

neverが判定できない問題
https://www.m3tech.blog/entry/2023/03/10/142235

never型以外のすべての型はnever型に代入できない

T extends never ? true : false; こうした場合、true型でもfalse型でもなくneverになるとのこと。

never型は空のunion型*2であり、空のunion型をdistributive conditional typeに渡しても適用されずneverになります。また、distributiveの性質をdisableすれば期待通りの挙動になります。

空のユニオンであれば、never

記事中に答えを見てしまったが、これはTSのDistributive をちゃんと理解していないと、答えだけ見ても意味がわからない。

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

以下でも解決できるとのこと。

type IsNever<T> = T[] extends never[] ? true : false;
type IsNever<T> = {t: T} extends {t: never} ? true : false;

A === never的な判定が出来ないので、上記の様になると理解しておこう。

high-ghigh-g

IsUnion

https://github.com/type-challenges/type-challenges/blob/main/questions/01097-medium-isunion/README.md

ユニオンの判断、感覚的にMappedTypesが利用できるかどうかで判断する気がする。
直感的に分からないので、裏仕様な気がする。

解答を見る。

TがTの場合(never対策ぽい)、CもTならtrue, そうでないならunknown, TがTじゃなければnever
()の固まりがtrue(条件が)であれば、falseを返し、そうでないならtrue。
複雑。。ユニオンの判定の場合、Distributiveの挙動の完全理解が重要そう。

type IsUnionImpl<T, C = T> = (T extends T ? C extends T ? true : unknown : never) extends true ? false : true;
type IsUnion<T> = IsUnionImpl<T>;

stringが適用されていた場合の動き。確かに実際に型をはめてみるとおいやすい。

IsUnion<string>
=> IsUnionImpl<string, string>
=> (string extends string ? string extends string ? true : unknown : never) extends true ? false : true
=> (string extends string ? true : unknown) extends true ? false : true
=> (true) extends true ? false : true
=> false

下記の変換をみると、一発目でneverかそうでないかの判定をしている。
(never extends neverはtrueでもbooleanでもなくneverなので。)
(string extends string ? string extends string ? true : unknown : never)

(string extends string ? true : unknown)

ユニオンが入った場合の例

IsUnion<string|number>
=> IsUnionImpl<string|number, string|number>
=> (string|number extends string|number ? string|number extends string|number ? true : unknown : never) extends true ? false : true
=> (
  (string extends string|number ? string|number extends string ? true : unknown : never) |
  (number extends string|number ? string|number extends number ? true : unknown : never)
) extends true ? false : true
=> (
  (string|number extends string ? true : unknown) |
  (string|number extends number ? true : unknown)
) extends true ? false : true
=> (
  (
    (string extends string ? true : unknown) |
    (number extends string ? true : unknown)
  ) |
  (
    (string extends number ? true : unknown) |
    (number extends number ? true : unknown)
  )
) extends true ? false : true
=> (
  (
    (true) |
    (unknown)
  ) |
  (
    (unknown) |
    (true)
  )
) extends true ? false : true
=> (true|unknown) extends true ? false : true
=> (unknown) extends true ? false : true
=> true

分配の挙動も分かりやすい

=> (string|number extends string|number ? string|number extends string|number ? true : unknown : never) extends true ? false : true
=> (
  (string extends string|number ? string|number extends string ? true : unknown : never) |
  (number extends string|number ? string|number extends number ? true : unknown : never)
) extends true ? false : true
high-ghigh-g

ReplaceKeys

https://github.com/type-challenges/type-challenges/blob/main/questions/01130-medium-replacekeys/README.md

ジェネリクスの意味について
1つ目→精査対象のオブジェクトのユニオン
2つ目→検索対象のキーのユニオン
3つ目→対象のキーの変換指定

改めてDistributive周りの挙動を見る
https://qiita.com/axoloto210/items/627b019995b81d918f7d

type A<T> = T extends string ? T[] : never;
A<'test1' | 'test2'> // ユニオンが分配され、'test1'[] | 'test2'[] となる。('test1' | 'test2')[] とはならない。

雰囲気こんな感じなのかな。

type ReplaceKeys<U, T, Y> =
  U extends Record<string, any> ? {
    Mapped Types
  } : U

ジェネリックの1つめがユニオンなので、それを分配しながらMapped Types内で更に条件分岐していく。

type ReplaceKeys<U, T, Y> =
  U extends Record<string, any> ? {
    [K in keyof U]: K extends T ? ここに処理を追加 : U[K]
  } : U

K(Uのキー)がTと一致している場合、Yのキーと値を元にUを変換していく感じかな。

KとTが一致する場合、KとYのキーが一致するものがあれば、Y側の値の型で変換。そうでなければnever。
書き方がわからない。。

文法的に間違ってるけど、やりたいことは今感じ。

type ReplaceKeys<U, T, Y extends Record<string, any>> =
  U extends Record<string, any> ? {
    [K in keyof U]:
      K extends T ?
        K extends keyof Y ? K[keyof Y]
        : U[K]
  } : U

解答を見る。

最もいいねが多い解答。
かなり近い所までいってた。
そうか、最初のオブジェクトかどうかのConditional Typesはいらない。
あとは、K extends keyof Yが担保できるならKはYの中にも存在するからY[K]がかけるし、そうでなければneverとできるのか。納得

type ReplaceKeys<U, T, Y> = { [K in keyof U]: K extends T ? K extends keyof Y ? Y[K] : never : U[K] }

他の解答も同じ様な感じだった。
もう一歩。

high-ghigh-g

Remove Index Signature

https://github.com/type-challenges/type-challenges/blob/main/questions/01367-medium-remove-index-signature/README.md

こういうことがしたい。
当たり前のように違う。

type RemoveIndexSignature<T extends {[key: string]: any}> = {
  [K in keyof T]: K extends [key: string | number | symbol] ? never : T[K]
}

Index Signatureを改めて復習

https://qiita.com/bearone236/items/b52261cc83b2d1026e75

オブジェクトの中で [key名: string]: 型名 で書く方法

{ [key: string]: string }

抜け道が分からない。。裏仕様な気がする。。
解答を見る。

1つ目

type RemoveIndexSignature<T, P=PropertyKey> = {
  [K in keyof T as P extends K ? never : K extends P ? K : never]: T[K]
}

ジェネリクスが一つ増えてP=PropertyKeyが書かれている。
PropertyKey型は string | number | symbol で表現されるユニオン型みたい。
https://zenn.dev/gemcook/articles/6b8fd769cd708a

Key RemappingでPがKに属するか、またKがPに属するかでプロパティを残すかどうかを判定してるみたい。
まだちょっと分かってない。

2つめ

type RemoveIndexSignature<Type> = {
    [
        Key in keyof Type as Key extends `${infer ConcreteKey}` ? ConcreteKey : never
    ]: Type[Key]
}

リテラルを通すことで、フィルタリングができるみたい。
テストケースは全ては通ってない。。

3つめ

type RemoveIndexSignature<T> = {
  [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K]
}

テストケースは全ては通ってない。
ただしくはこういうことみたい。

  [K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K ]: T[K]

↓が直感的に浮かんできそうだけど、Union型でConditional Typeの記述をした場合、分配されるからだめ。
(A | B) extends Tは(A extends T | B extends T) となる。

// これは期待通りに動作しない
type RemoveIndexSignature<T> = {
  [K in keyof T as (string | number | symbol) extends K ? never : K]: T[K]
}

なので、一つずつ確認していく必要がある。

個人的な理解だけど、index signatureで書かれた記述以外の普通のプロパティは、Kに対してリテラルが入ってくるからstring extends Kのチェックもすり抜けられて、逆にindex signatureで定義されている場合、string | number | symbolのどこかで引っかかるという理解。

high-ghigh-g

Percentage Parser

https://github.com/type-challenges/type-challenges/blob/main/questions/01978-medium-percentage-parser/README.md

expectedの値を見ると、必ず3つの値をもつstringArray型で、
1つ目に符号、2つ目に値、3つ目に%となっている。

ジェネリクスに入る型はstringで固定なので、inferを使ったリテラル型の操作な感じ
array型にindex指定で型を入れる方法が分からないけど、一旦これは考えなくても良さそう。

リテラルの分解を二段階でやる必要がある。
めちゃくちゃダサいけど、とりあえず正解できた。

type isSign<S extends string> = S extends '+' ? true : S extends '-' ? true : false;

type PercentageParser<A extends string> =
  A extends `${infer First}${infer Other}` ?
    isSign<First> extends true ?
      Other extends `${infer B}%` ?
        [First, B, '%'] : [First, Other, '']
    :
      A extends `${infer C}%` ?
        ['', C, '%'] : ['', A, '']
  :
    ['', '', '']

解答を見る
最もいいねの多い解答。

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

符号の判定が賢い。+か-なら、そのまま返し、そうでなければneverでいいのか。
%の判定も賢い。%のありなしで配列型を作ってしまってる。
最後の配列の連結も賢すぎる。。スプレッド構文を使えば良いのか。

2つ目

type PercentageParser<A extends string> = A extends `${infer L}${infer R}`
  ? L extends '+' | '-'
    ? R extends `${infer N}%` ? [L, N, '%'] : [L, R, '']
    : A extends `${infer N}%` ? ['', N, '%'] : ['', A, '']
  : ['', '', '']

直列で記述しているパターンもシンプルでよい。
extendsを2度書きそうになったら、ユニオンで一つにまとめられないかを考慮する。

high-ghigh-g

Drop Char

https://github.com/type-challenges/type-challenges/blob/main/questions/02070-medium-drop-char/README.md

ちょっとダサめだけど出来た。

type DropChar<S extends string, C extends string> =
  S extends `${C}${infer Last1}`
    ?
      DropChar<Last1, C>
    :
      S extends `${infer First2}${C}${infer Last2}`
        ?
          DropChar<`${First2}${Last2}`, C>
        :
          S extends `${infer First3}${C}`
            ?
              DropChar<`${First3}`, C>
            :
              S

解答を見る。
一番goodが多い解答。

type DropChar<S, C extends string> = S extends `${infer L}${C}${infer R}` ? DropChar<`${L}${R}`, C> : S;

これでいけるんだ。
なるほど。。LやRが空文字だったとしても、LCRの形が成り立つからこの記述でいいのか。勉強になる。

high-ghigh-g

MinusOne

https://github.com/type-challenges/type-challenges/blob/main/questions/02257-medium-minusone/README.md

いよいよTSの型システムで演算ができる様な感じになってきた。。

流石にそういうのは出来ない。

type MinusOne<T extends number> = T-1

裏仕様にしか見えないので早速解答を見る。
やはり、TSできれいな演算など出来なく、泥臭いやり方でなんとか-1の実装をしている

一番分かりやすそうなものからロジックをみてみる。

// your answers
type DigitToArray = {
  "0": [],
  "1": [unknown],
  "2": [unknown, unknown],
  "3": [unknown, unknown, unknown],
  "4": [unknown, unknown, unknown, unknown],
  "5": [unknown, unknown, unknown, unknown, unknown],
  "6": [unknown, unknown, unknown, unknown, unknown, unknown],
  "7": [unknown, unknown, unknown, unknown, unknown, unknown, unknown],
  "8": [unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown],
  "9": [unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown]
};
type CreateArrayByLength<N extends string, R extends unknown[] = []> = N extends `${infer First}${infer Rest}`
? First extends keyof DigitToArray
  ? CreateArrayByLength<Rest, [...R, ...R, ...R, ...R, ...R, ...R, ...R, ...R, ...R, ...R, ...DigitToArray[First]]>
  : never
: R;
type MinusOne<T extends number> = CreateArrayByLength<`${T}`> extends [infer First, ...infer Rest]
? Rest["length"]
: never;

ジェネリクスをリテラルに変換し、CreateArrayByLengthという型に渡している。
その中で、inferを使って、リテラルを分解し、FirstとDigitToArrayのキー(0〜9)と一致しているかどうかを判定。
一致しているなら、レスト構文で...DigitToArray[First]とし、unknown配列が格納された配列型を展開している。
最終的にunknownの数をlengthで読み取っている感じ。
このロジックだと、負の数は表現できないし、途中で_などの3桁区切りが入った場合も表現できていないのがネック。

あと、なぜ...Rが10こあるのか

AIに聞いてみた。

Result2の場合、N"10"です。

まず "1"が処理されます。DigitToArray["1"][unknown]なので、R[unknown] になります。
次に "0"が処理されます。DigitToArray["0"][]なので、R[unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown] となります。

なるほど、10をそのまま処理すると、[unknown]だけになってしまうけど、10の位を表現する為に、..Rが10になってるのか。
10の位が0の場合でも、unknownが埋められるのか。

配列型とレスト構文を使った、値の埋め方、lengthの使い方は重宝しそう。
上記を理解したうえで次

type Pop<T extends any[]> = T extends [...infer head, any] ? head : never; 

type MinusOne<T extends number, A extends any[] = []> = A['length'] extends T
  ? Pop<A>['length']
  : MinusOne<T, [...A, 0]>

type Pop<T extends any[]> = T extends [...infer head, any] ? head : never;
が学び。
A extends [...infer head, any] ? head: never で末尾を抽出することが出来る。リテラルでも使えそう。

下記だと、テストは100の位までしか表現できていなさそう。
1000の位がきた時に以下のエラーが出ている
Type instantiation is excessively deep and possibly infinite.(2589)
(型推論が深すぎる、もしくは無限ループに陥っている可能性)
ネストに限りがあるのか。。

次、一番goodが多かったやつ。

type ParseInt<T extends string> = T extends `${infer Digit extends number}` ? Digit : never
type ReverseString<S extends string> = S extends `${infer First}${infer Rest}` ? `${ReverseString<Rest>}${First}` : ''
type RemoveLeadingZeros<S extends string> = S extends '0' ? S : S extends `${'0'}${infer R}` ? RemoveLeadingZeros<R> : S
type InternalMinusOne<
  S extends string
> = S extends `${infer Digit extends number}${infer Rest}` ?
    Digit extends 0 ?
      `9${InternalMinusOne<Rest>}` :
    `${[9, 0, 1, 2, 3, 4, 5, 6, 7, 8][Digit]}${Rest}`:
  never
type MinusOne<T extends number> = ParseInt<RemoveLeadingZeros<ReverseString<InternalMinusOne<ReverseString<`${T}`>>>>>
type test = MinusOne<9007199254740992>

number型のジェネリクスをリテラル化→文字を逆転

S extends ${infer Digit extends number}${infer Rest} 学び。リテラルの中でinferを使うことで、numberかどうかを判定できるみたい。

${[9, 0, 1, 2, 3, 4, 5, 6, 7, 8][Digit]} ここも賢い。Digitに入る数値に応じて配列内の値を取得している。
こんなこともできるのか。

空文字かどうかを最初に判定しているから、文字を逆転していることで、0→9と決め打ちしてそう。
かなりトリッキー。

数値判定処理が終わったら、再度文字を逆転して、並びをもとに戻している。
先頭の0を削除→ParseInt

type ParseInt<T extends string> = T extends ${infer Digit extends number} ? Digit : never

なるほど、リテラルの中がnumberと判断できるならinferでその対象を取得してるのか。

high-ghigh-g

PartialByKeys

https://github.com/type-challenges/type-challenges/blob/main/questions/02757-medium-partialbykeys/README.md

こんな感じで考えたけど違う。

type PartialByKeys<T extends Record<string, any>, K extends keyof T> = {
  [S in K]?: T[S]
} & {
  [P in keyof T as P extends K ? never : P]: T[P]
}

解答を見る。
👍️が一番ついているもの。

type IntersectionToObj<T> = {
  [K in keyof T]: T[K]
}
type PartialByKeys<T , K = any> = IntersectionToObj<{
  [P in keyof T as P extends K ? P : never]?: T[P]
} & {
  [P in Exclude<keyof T, K>]: T[P]
}>

?をつける位置はあってた。
インターセクション型を再度オブジェクトにしないと駄目なのか。
type IntersectionToObj<T> = { [K in keyof T]: T[K] } それ自体はシンプルな自作ユーティリティ型。

2つ目、バリバリユーティリティ型をつかってる。

type PartialByKeys<T extends {}, U = keyof T> = 
  Omit<Partial<Pick<T, U & keyof T>> & Omit<T, U & keyof T>, never>

Omitを利用すると、自作のオブジェクト生成ユーティリティを使わなくて良いのか。学び。

Pick → 第一型引数のオブジェクト型から第二型引数(ユニオン型)で指定したキーだけで構成されたオブジェクト型を返すユーティリティ型

Omit<オブジェクト型, never> は、オブジェクトの交差型を単一のオブジェクト型にするTipsってことか。

Partial → 与えられたオブジェクト型のキーを全てOptionalにするユーティリティ型

Omit → 与えられたオブジェクト型から指定したキーを除いたオブジェクト型を返すユーティリティ型

high-ghigh-g

RequiredByKeys

https://github.com/type-challenges/type-challenges/blob/main/questions/02759-medium-requiredbykeys/README.md

前回課題と逆の様な実装をすればokかな?と思ったらテスト通らない

type RequiredByKeys<T extends {[K in string]?: any}, K extends keyof T = keyof T> = Omit<{
  [U in K]: T[U]
} & {
  [O in keyof T as O extends K ? never : O]?: T[O]
}, never>

解答を見てみる。

1つ目

type RequiredByKeys<
  T, 
  K extends keyof T = keyof T,
  O = Omit<T, K> & { [P in K]-?: T[P] }
> = { [P in keyof O]: O[P] }

3つめのジェネリクスを作成し、その中でOmit<T, K> & Mappedの展開をしている。
-?の書き方は初めて見た。
OをMappedTypesで回してるのシンプル。
シンプルな型を書く人はジェネリクスを変数として扱う方法が上手。

2つめ

type RequiredByKeys<T , K = keyof T> = Omit<T & Required<Pick<T,K & keyof T>>, never>

Requiredを使っている。K & keyof Tで、
こういう課題をシンプルに表現できるかどうかは、ユーティリティ型の知識の有無な気がしてきた。
Omit<T, never>は定番。

あと今回はインターセクションする場合の挙動を理解しているかどうかも大きいと感じた。

ユニオンが和集合で、インターセクションは積集合。
&が使われているので、足されているイメージを持ちそうだが、共通部分。
まさに AND の意味のまま使われている。

https://qiita.com/ist-ko-su/items/af8224f7571817fbb9bd

high-ghigh-g

Mutable

https://github.com/type-challenges/type-challenges/blob/main/questions/02793-medium-mutable/README.md

readonlyを取り払う。
Mapped Typesをかければ良いわけじゃない。
readonlyかどうかの判定はどうするか。

解答を見る。

type Mutable<T> = {-readonly [K in keyof T]: T[K]}

Mapped Typesに -readonly をつければよい。
- って何!初めて知った。

ちゃんと仕様に書いてあった。
-? でオプショナルも消せる(前の課題でやったやつ。)
https://typescriptbook.jp/symbols-and-keywords#--修飾子の削除-ts

high-ghigh-g

ObjectEntries

https://github.com/type-challenges/type-challenges/blob/main/questions/02946-medium-objectentries/README.md

こういうのは思いついた。

type ValueOf<T> = T extends {[Key in keyof T]: infer U} ? U : never
type ObjectEntries<T> = ValueOf<{[K in keyof T]: [K, T[K]]}>

Expect<Equal<ObjectEntries<Partial<Model>>, ModelEntries>>, のテストケースだけが満たせていない。Partialはキーをオプショナルにするユーティリティ型。

オプショナルかどうかの判定をどうするかが分からない。

解答を見る。

一番シンプルそうなのは以下

type ObjectEntries<T,U = Required<T>> = {
  [K in keyof U]:[K,U[K]]
}[keyof U]

オブジェクト型に[keyof U]で値をユニオンで書き出せるのか。インデックス型も分配できる学び。
ただ、 Expect<Equal<ObjectEntries<{ key?: undefined }>, ['key', undefined]>>, のテストケースが満たせていない。

type RemoveUndefined<T> = [T] extends [undefined] ? T : Exclude<T, undefined>
type ObjectEntries<T> = {
  [K in keyof T]-?: [K, RemoveUndefined<T[K]>]
}[keyof T]

この解答に、インデックス型の分配についても書かれていた。ただし、満たせていないテストケースもある。
https://github.com/type-challenges/type-challenges/issues/14052

string | undefinedの表現がMapped Typesだとできていない。

以下が全テストケースをクリアできていた。

type ObjectEntries<T, U = Required<T>> = {
  [K in keyof U]: [K, U[K] extends never ? undefined : U[K]]
}[keyof U]

ジェネリクスの2つ目の引数にRequired<T>とし、それをループするのが必要っぽい。
オプショナルに振り回されない為か。
U[K] extends never ? undefined : U[K] U[K]がneverならundefinedとなるのか。
AIに効いたら、U[K]がnever型となるとき、undefinedとなる仕様を利用しているみたい。(これは前に見た。)
なるほど。

これはTSの仕様を正しく理解していないと解けない問題だった。

high-ghigh-g

Tuple to Nested Object

https://github.com/type-challenges/type-challenges/blob/main/questions/03188-medium-tuple-to-nested-object/README.md

本当にざっくり、最初の段階として以下を考えてみた。

type TupleToNestedObject<T extends string[], U> =
  T extends [] ? U :
    T extends [infer F, ...any] ? {F: U}: never

infer Fをキーとして扱う書き方が分からない。
あとは、ネストで展開していくために以下を多段階で展開する必要がある。

Fの部分でエラーが表示されてるけど、一旦テストケースは一つクリアできてる。
入れ子の対応

type TupleToNestedObject<T extends string[], U> =
  T extends [] ? U :
    T extends [infer F, ...any] ? {[key in F]: U}: never

ネスト対応は以下で対応してみた。エラーがでてるので少し違いそう。

type TupleToNestedObject<T extends string[], U> =
  T extends [] ? U :
    T extends [infer F, ...infer Other] ?
      {[key in F]: Other extends '' ? U: TupleToNestedObject<[...Other], U>}  : never

解答を見る。
1つ目。

type TupleToNestedObject<T, U> = T extends [infer F,...infer R]?
  {
    [K in F&string]:TupleToNestedObject<R,U>
  }
  :U

Tが配列型であれば、Mapped TypesでFをキーにし、値を再帰する形。
配列の扱い方がシンプル。
配列にinferを使うパターンだと、配列が空の場合は考慮しなくて良いみたい。
Fを型内でキャスト?絞る?ときに&stringという書き方をするのか。

↓良さそう。

type TupleToNestedObject<T, U> = T extends [infer F extends PropertyKey,...infer R]?
  {[K in F] : TupleToNestedObject<R,U>} : U

infer Fに対してextends PropertyKeyで制約が付けれるみたい。
inferに使えるの初めて知った。

high-ghigh-g

Reverse

https://github.com/type-challenges/type-challenges/blob/main/questions/03192-medium-reverse/README.md

できた。

type Reverse<T extends string[]> = T extends [infer First, ... infer Other extends string[]] ? [...Reverse<Other>, First] : []

必要な知識

  • ジェネリクス制約
  • infer
  • inferに対しての制約
  • 再帰
  • レスト構文

解答を見る。

1つ目。

type Reverse<T extends any[]> = T extends [infer F, ...infer Rest] ? [...Reverse<Rest>, F] : T;

別にジェネリクス制約にstring[]を指定する必要なかった。
any[]でいい。テストケースに影響されずに汎用的な型を考えることを忘れない。

high-ghigh-g

Flip Arguments

https://github.com/type-challenges/type-challenges/blob/main/questions/03196-medium-flip-arguments/README.md

関数の引数の順序を逆転させる問題とのこと
引数の指定でinferを使う記述が文法的に忘れてる。

調べたらこうか
(...arg: infer U) => any

普段使ってないから忘れてた。

無限ループする型になる。

type FlipArguments<T> = T extends (f: infer First, ...arg: infer U) => infer S ? FlipArguments<(arg: U, f: First) => S> : never 

解答を見る。

1つ目

type Reverse<T extends unknown[]> = T extends [infer F, ...infer R] ? [...Reverse<R>, F] : [];

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

引数をまるっと受け取って、順序を逆転させる自作ユーティリティ型が作られている。
引数の型は、unknown[]またはany[]になることを覚えておかないといけない。
Reverseのロジックは配列型の順序を入れ替えるものと同じ。
本体のFlipARgumentsは、至って普通な型定義になっている。
ただし、関数に対してinferを利用する場合は、 (...arg: infer P) => unfer U という形になることを覚えておかないといけない。
それ以外の部分は普通のConditional Types。

high-ghigh-g

FlattenDepth

https://github.com/type-challenges/type-challenges/blob/main/questions/03243-medium-flattendepth/README.md

配列型の展開について
一旦以下を書いてみた。いくつかのテストケースがクリアできていない。

type FlattenDepth<T extends any[]> = 
  T extends [infer F, ...infer R] ? [F extends [infer U] ? U : F, ...FlattenDepth<R>] : []

解答を見る

シンプルそうなやつ

type FlattenDepth<T extends any[], C extends number = 1, U extends any[] = []> = T extends [infer F,...infer R]?
  F extends any[]?
    U['length'] extends C?
      [F, ...FlattenDepth<R, C, U>]
      :[...FlattenDepth<F, C, [0,...U]>,...FlattenDepth<R, C, U>]
    :[F,...FlattenDepth<R, C, U>]
  : T;

ジェネリクスが3つ用意されている。Cがnumber型, Uが配列型。
Fに対し、any[]で判定、さらに、U[length]とCが一致しているかを判定し、
Fが配列型でなければ、[F, ...FlattenDepth<R, C, U>]
Fが配列型で、lengthが1かどうかで更に、
[F, ...FlattenDepth<R, C, U>]とするか
[...FlattenDepth<F, C, [0,...U]>,...FlattenDepth<R, C, U>] とするかで分岐

なるほど。
パターンに分けてちゃんと実装する方法を丁寧に考えないと。
課題が多段階で思考する必要がある場合に早々に諦めがち。

high-ghigh-g

BEM style string

https://github.com/type-challenges/type-challenges/blob/main/questions/03326-medium-bem-style-string/README.md

MappedTypesで連結して最後に M['length'] で対応できるかも?
こういうの書いてみたけど違う。

type GetUnion<Base extends string, T extends string[], sign extends string = '__'> = {
  [K1 in keyof T]: `${Base}${sign}${T[K1]}`
}['length']

type BEM<B extends string, E extends string[], M extends string[]> = GetUnion<B, E, '__'>

解答を見る。
lengthじゃない、numberか。lengthは要素数を取得するやつだよ。。

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

InorderTraversal

https://github.com/type-challenges/type-challenges/blob/main/questions/03376-medium-inordertraversal/README.md

val, left, rightをプロパティに持つObjectから配列型を作る。
配列型の値を格納していくジェネリクスが必要そう。
leftがnullならvalを配列に格納し、rightなら再帰的な精査をする感じかな。

普段 type + インターセクション型 での継承もどきしかしていないからinterface + extendsの正式な継承を忘れている。
https://typescriptbook.jp/reference/object-oriented/interface/interface-inheritance

書いてみた。雰囲気は以下のようなことがしたい。

interface TreeNode {
  val: number
  left: TreeNode | null
  right: TreeNode | null
}

interface TreeNodeLeftNull extends TreeNode {left: null; right: TreeNode}
interface TreeNodeRightNull extends TreeNode {left: TreeNode; right: null}
interface TreeNodeBothNull extends TreeNode {left: null; right: null}

type InorderTraversal<T extends TreeNode | null, Result extends number[] = []> =
  T extends TreeNodeLeftNull ?
    InorderTraversal<T, [T['val'], any]> :
    T extends TreeNodeRightNull ? 
       InorderTraversal<T, [...any, T['val']]> :
       T extends TreeNodeBothNull ? InorderTraversal<null, [...any, T['val']]> : Result

解答を見る。

もっとも👍️がついている解答

type InorderTraversal<T extends TreeNode | null, NT extends TreeNode = NonNullable<T>> = T extends null
  ? []
  : [...InorderTraversal<NT['left']>, NT['val'], ...InorderTraversal<NT['right']>]

NonNullableユーティリティユニオンからnullを取り除いた型を返す。
https://typescriptbook.jp/reference/type-reuse/utility-types/nonnullable

Tがnullだったら[]を返す。
配列型内にまとめてleft, val, rightを指定しているの賢い。
こういう構想がまだできないの辛い。

NT['left']また、NT['right']が TreeNode | nullだからそれをレスト構文で返せるのか。

2つめ。

type InorderTraversal<T extends TreeNode | null> = [T] extends [TreeNode]
  ? [...InorderTraversal<T['left']>, T['val'], ...InorderTraversal<T['right']>]
  : [];

構想は1つ目と同じ。
なんでわざわざ[T]としてるかは気になる。
別にT extends TreeNodeでも動いた。

これが一番シンプルで良さそう。

type InorderTraversal<T extends TreeNode | null> = T extends TreeNode
  ? [...InorderTraversal<T['left']>, T['val'], ...InorderTraversal<T['right']>]
  : [];

配列展開系の場合、ジェネリクスなどを使わずに真っ先に直接格納 + レスト構文を使うようにする。

high-ghigh-g

Flip

https://github.com/type-challenges/type-challenges/blob/main/questions/04179-medium-flip/README.md

ぱっと思いついたコード

type Flip<T extends Record<any, any>> = {
  [K in keyof T as T[K]]: K
}

このテストコードだけみたせない。

  Expect<Equal<{ 3.14: 'pi', true: 'bool' }, Flip<{ pi: 3.14, bool: true }>>>,

3.14やtrueの部分がキーの時はstringだが、右側に移した場合にキャストしないといけなさそう?

下記を書いてみたけど違う。

type Flip<T extends Record<any, any>> = {
  [K in keyof T as T[K]]: K extends `${infer V}` ? V : never
}

解答を見る。

もっとも👍️が多いもの

type Flip<T extends Record<string, string | number | boolean>> = {
  [P in keyof T as `${T[P]}`]: P
}

as の右側がリテラルになっている。
キーの部分にstring以外が入った場合にエラーとなってそう。

high-ghigh-g

Fibonacci Sequence

https://github.com/type-challenges/type-challenges/blob/main/questions/04182-medium-fibonacci-sequence/README.md

フィボナッチ数列・・・なんだっけ。。
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144,
1つまえと2つまえの和で成り立つ数列か。
げきむずな雰囲気。。

いけてない型定義でテストケースはクリアできた。

type FibonacciArray = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144]
type Fibonacci<T extends number> = FibonacciArray[T]

解答を見る

最も👍️がおおい解答

type Fibonacci<
    T extends number,
    CurrentIndex extends any[] = [1],
    Prev extends any[] = [],
    Current extends any[] = [1]
> = CurrentIndex['length'] extends T
    ? Current['length']
    : Fibonacci<T, [...CurrentIndex, 1], Current, [...Prev, ...Current]>

とりあえず天才。
ジェネリクスT と CurrentIndex['length']が一致しているかどうか
CurrentIndexは、初期値で[1] のみが格納されたnumber配列型
CurrentもCurrentIndexの様な感じだけど、最も右側に来る値として設定している
あと、Prevもある
一致している場合は、そのときのCurrentIndexのlengthを返す
それ以外の場合は、Fibonacciを再帰で回しながら1の数を増やしていく。
足し算系のロジックは配列型 + レスト構文 + 再帰で回すって前にやったなあ。。

type Fibonacci<
  T extends number,
  No extends 1[] = [1, 1, 1],
  N_2 extends 1[] = [1],
  N_1 extends 1[] = [1]
> = T extends 1 | 2
  ? 1
  : T extends No['length']
  ? [...N_2, ...N_1]['length']
  : Fibonacci<T, [...No, 1], N_1, [...N_2, ...N_1]>

上記は、先頭と2番目(インデックスが1番目)とそれ以外のパターンで分けた型定義。
こういう型で計算する系のお題の解答は天才で溢れてる。

high-ghigh-g

AllCombinations

https://github.com/type-challenges/type-challenges/blob/main/questions/04260-medium-nomiwase/README.md

一旦考えた型

type AllCombinations<S extends string> = S extends `${infer F}${infer Rest}` ? F | AllCombinations<Rest>: '' 

例えば、
'AB' → '' | 'A' | 'B' | 'AB' | 'BA'
このパターンのAB, BAが満たせてない

これも考えてみたけど、文字が増えると対応できない。

type AllCombinations<S extends string> = S extends `${infer F}${infer Rest}` ? S | F | `${Rest}${F}` | AllCombinations<Rest>: '' 

1.先頭文字のみを取得したもの → これはok
2.先頭文字の組み合わせ(1文字+1文字)
3.先頭文字+他のn文字の組み合わせ

各パターンを配列型に詰めたほうがいいのか?

解答を見る。

もっともいいねが多い解答。

type String2Union<S extends string> =
  S extends `${infer C}${infer REST}`
  ? C | String2Union<REST>
  : never;

type AllCombinations<
  STR extends string,
  S extends string = String2Union<STR>,
> = [S] extends [never]
  ? ''
  : '' | {[K in S]: `${K}${AllCombinations<never, Exclude<S, K>>}`}[S];

String2Unionで文字列を再帰的にバラして1文字のリテラルユニオンを作ってる。
そのリテラルユニオンをAllCombinations内でMappedTypesを使って展開している。

${K}${AllCombinations<never, Exclude<S, K>>}}[S] が思ったよりも高度なことしてる。。

Exclude ユニオンから指定したリテラル or ユニオンを取り除いたユニオンを返すユーティリティ
https://typescriptbook.jp/reference/type-reuse/utility-types/exclude

{K}{AllCombinations<never, Exclude<S, K>>}: これは、各キー K に対する値を定義しています。

↓生成AIに聞いてみた。
K: 現在の文字。
AllCombinations<never, Exclude<S, K>>: 現在の文字を除く他のすべての文字のすべての可能な組み合わせを再帰的に生成します。
{K}{...}: 現在の文字と、残りの文字の組み合わせを連結した文字列を作成します。

それを[S]でindex signatureで参照することで、ユニオンが作れるのか。

ほかの解答をみてもExcludeでユニオンから対象のリテラルをとに除きつつ、全体を再度リテラルで結合する書き方をしていた。

`${U}${AllCombinations<S, Exclude<T, U>>}`
high-ghigh-g

Greater Than

https://github.com/type-challenges/type-challenges/blob/main/questions/04425-medium-greater-than/README.md

数値型の比較がでてきた。もちろん比較記号は使えない。
数値型がわたってきたらその数値型と同じ要素数の配列型を作りたい。

とりあえず自作ユーティリティ作った。

type CreateArr<N extends number, ARR extends number[] = []> = N extends ARR['length'] ? ARR : CreateArr<N, [1, ...ARR]>

これをTとUの分を作って、T側の配列型をMappedTypesで回して、Uの要素をConditionalTypesで確認し、とりあえず存在しないならtrueとかで良さそう。
と書いてみたもののやはりだいぶん難しい。

雰囲気はこういうことがしたい。(なぐり書きしたので、エラーもあるし、インデントもきたない。)

type CreateArr<N extends number, ARR extends number[] = []> = N extends ARR['length'] ? ARR : CreateArr<N, [1, ...ARR]>
type GreaterThan<T extends number, U extends number, ARR1 extends number[] = CreateArr<T>, ARR2 extends number[] = CreateArr<U>> = {
  [K in keyof ARR1]: ARR1[K] extends ARR2[K] ? 1 : never
} extends number[] ? {
  [K in keyof ARR1]: ARR1[K] extends ARR2[K] ? 1 : null
}['length'] extends null ? true: false
: never

ジェネリクスを一時メモリとして利用する記述が分かってきた。

解答を見る。

最もいいねの多い解答

type ArrayWithLength<T extends number, U extends any[] = []> = U['length'] extends T ? U : ArrayWithLength<T, [true, ...U]>;
type GreaterThan<T extends number, U extends number> = ArrayWithLength<U> extends [...ArrayWithLength<T>, ...infer _] ? false : true;

与えられた数値型を元に配列型を作るノウハウは理解したし、ここの方向性はあってる。
なるほど、
ArrayWithLength<U> extends [...ArrayWithLength<T>, ...infer _] ? false : true;
TとUで生成した配列の形をConditional Typesで直接比較して、inferの部分に要素があるかないかで判定するのか。

自作ユーティリティ型を作らずに直接ConditionalTypesと再帰している解答もあった。

type GreaterThan<T extends number, U extends number, R extends any[] = []> = 
  T extends R['length']
    ? false
    : U extends R['length']
      ? true
      : GreaterThan<T, U, [...R, 1]>

ほとんどのテストケースはクリアできているが、bigint的な数値の場合は無理そう。
どのみちType instantiation is excessively deep and possibly infinite.のエラーが表示される。

high-ghigh-g

Zip

https://github.com/type-challenges/type-challenges/blob/main/questions/04471-medium-zip/README.md

とりあえずぱっと思いついたやつ。

type Zip<T, U> = {
  [K in keyof T]: K extends keyof U ? [T[K], U[K]] : never
}

Expect<Equal<Zip<[1, 2, 3], ['1', '2']>, [[1, '1'], [2, '2']]>> このテストケースが満たせてない。

解答を見る。

type Zip<A extends any[], B extends any[], L extends any[] = []> = L['length'] extends A['length'] | B['length']
  ? L
  : Zip<A, B, [...L, [A[L['length']], B[L['length']]]]>

めちゃくちゃ複雑なんですけど、、
[...L, [A[L['length']], B[L['length']]]]
なるほど、Lの要素が再帰の回を増すごとに増えていくことを利用してるのか。
再帰的に回して作った配列型をリターン

high-ghigh-g

IsTuple

https://github.com/type-challenges/type-challenges/blob/main/questions/04484-medium-istuple/README.md

とりあえず書いてみた。書いてみただけになってる。

type IsTuple<T> = T extends [] ?
  true :
    T extends (infer F)[] 
      ? F extends PropertyKey ?
        false : true
      : false

↓IsUnionで検索したら出てきた。型分配ができるかどうかでの判定か。
https://zenn.dev/pokotyan/articles/fd47f277ed80c0#union-distribution

上記を応用してさらに記述してみたけど解けない。。

type IsTuple<T> = T extends [] ?
  true :
    T extends (infer T2)[]
      ? T2 extends PropertyKey
        ? false :
          [T2] extends [T] ? false : true
      : false

解答を見る。

type IsTuple<T> = 
  T extends readonly any[]?
    number extends T['length']?false:true
  :false

number extends T['length']の部分、Conditional Typesが逆転しててもいいのか

どうも初見殺しな解らしい。
https://zenn.dev/pokotyan/articles/fd47f277ed80c0#タプル

Expect<Equal<IsTuple<never>, false>>のテストケースでエラーが出ていたので修正。

type IsTuple<T> = 
  [T] extends [never] ? false :
    T extends readonly any[]?
      number extends T['length']?false:true
    :false
high-ghigh-g

Chunk

https://github.com/type-challenges/type-challenges/blob/main/questions/04499-medium-chunk/README.md

lodashでいうところのchunkライクな方を実装する案件。
配列型内をジェネリクスで指定された数値型でくくりだし、配列型の入れ子的なようなものを作るみたい。

個数を元にループを回すやり方前もあった気がする。

再帰で回しながら配列の個数を増やして、lengthと一致した時にその時の方を返すような感じ。
配列を作りながら再帰で回す。むずい。まず1つ分の配列を作ることに集中しよう。

type CreateArr<
  Num extends number = 0,
  BaseArr extends number[] = [],
  ProcCountArr extends number[] = [],
  AllArr extends any[] = [], // chunkライクな型全体を保持するジェネリクス
  TmpArr extends any[] = [] // hunkライクな型の中の単一の方を保持する
> = ProcCountArr['length'] extends AllArr['length']
  ? AllArr
  : ???

こういうのを考える時に変にカウントアップする為の道具だったり一時保存的な内容を考える思考になるのは、自分自身にプログラミングセンスが無いからだと思ってしまう。

解答を見る。
最もいいねが多い解答。

type Chunk<T extends any[], N extends number, Swap extends any[] = []> =
Swap['length'] extends N
  ? [Swap, ...Chunk<T, N>]
  : T extends [infer K, ...infer L]
    ? Chunk<L, N, [...Swap, K]>
    : Swap extends [] ? Swap : [Swap]

配列型Tとnumber型Nを受け取って、都度型の状態を保存するSwapが用意されている。
Swapの長さとNが一致する場合、[Swap, ...Chunk<T, N>] として、Swapの長さがクリアされるようになってる。
なるほど、個別の配列の長さとNを比べて、配列の中だけを再帰で動かすのか。
再帰する部分を見極めるのが、処理をシンプルに書くうえでの秘訣かもしれない。
上記以外の場合、T extends [infer K, ...infer L]の形なら、infer Lで切り取った部分をChunkに投げてを再帰で動かすのか。
Swap extends [] ? Swap : [Swap]
Swapが空配列なら、そのままSwapをリターンし、そうでないならSwapを入れ子にして返す。[ [xx] ] 的な。

シンプルな解答。ほかも見てみる。

type Chunk<
  T extends any[],
  U extends number = 1,
  S extends any[] = [],
  V extends any[] = [],
> = T extends [infer F, ...infer R]
  ? S['length'] extends U
    ? Chunk<R, U, [F], [...V, S]>
    : Chunk<R, U, [...S, F], V>
  : S['length'] extends 0 
    ? V : [...V, S]

こっちのほうが構造は分かりやすいし、元々目指していたものとも近い。
S extends any[] = [], // 単体分Swap領域
V extends any[] = [], // 全体分Swap領域
という変数的に利用するジェネリクスを用意。
T extends [infer F, ...infer R]と一致する型であれば、

S['length'] extends U
? Chunk<R, U, [F], [...V, S]>
: Chunk<R, U, [...S, F], V>

配列型の入れ子になっている単体の型と向き合える。
が、言語化がむずい。(頭が働いていない。)
S['length']とU(第2引数)が一致する時
Chunk<R, U, [F], [...V, S]>
R → inferで取得した大元の配列型の先頭より後ろ
U → 元のnumber型
[F] → ここが言語化しにくい。なぜ先頭のリテラル。おそらく、S['length']の場合は、Vが、[...V, S]とされているので、Sの値をリセットする意味でFが格納されてそう。
[...V, S] →値の上書き

Chunk<R, U, [...S, F], V>
こっちは分かりやすい
[...S, F] → レスト構文でSの中身を展開しつつ、F(先頭のリテラル)を追加
Vはそのまま。

high-ghigh-g

Fill

https://github.com/type-challenges/type-challenges/blob/main/questions/04518-medium-fill/README.md

不等号系の処理の実装苦手・・・。少し前にあった気がする。

↓自分の思考をダンプ
Conditinal Types + inferで、A extends [...infer B, ...infer R] の形を満たしていればBはA以上と表すことが出来る。
なので、数値A < 数値Bとするときは、数値を元に配列を作成するユーティリティ型が必要。

この辺りが最初からユーティリティとして欲しい。(競プロ的なことにしか使わないだろうけど)

type CreateArr<Num extends number, Arr extends number[] = []> = Num extends Arr['length'] ? Arr : CreateArr<Num, [Num, ...Arr]>

type GreaterThan<Arr1 extends number[] = [], Arr2 extends number[] = []> = Arr2 extends [...Arr1, ...infer _] ? true : false

一旦、以下の様な型定義を書いてみた

/* 数値型の数と同じ個数の配列型を作成 */
type CreateArr<Num extends number, Arr extends number[] = []> = Num extends Arr['length'] ? Arr : CreateArr<Num, [Num, ...Arr]>

/* Arr2.lengthがArr1.length以上ならtrue, そうでなければfalse */
type GreaterThan<Arr1 extends number[] = [], Arr2 extends number[] = []> = Arr2 extends [...Arr1, ...infer _] ? true : false

type Fill<
  T extends unknown[],
  N,
  Start extends number = 0,
  End extends number = T['length'],
> = {
  [K in keyof T]:
    K extends number 
      ? GreaterThan<CreateArr<Start>, CreateArr<K>> extends true
        ? N
        : T[K]
      : never
}

↓ [never, never, never] になる。Kがそもそもnumberじゃない。

type Res = Fill<[1, 2, 3], 0, 0, 0>

根本の考えが間違えてるみたい。。
解答を見る。

type Fill<
  T extends unknown[],
  N,
  Start extends number = 0,
  End extends number = T['length'],
  Count extends any[] = [],
  Flag extends boolean = Count['length'] extends Start ? true : false
> = Count['length'] extends End
  ? T
  : T extends [infer R, ...infer U]
    ? Flag extends false
      ? [R, ...Fill<U, N, Start, End, [...Count, 0]>]
      : [N, ...Fill<U, N, Start, End, [...Count, 0], Flag>]
    : T

順番に見ていく。
ジェネリクスは最初から記述されている T, N, Start, End に加えて Count と Flag が追加されている。
Count['length']とEnd(T['length']) が同じ場合、そのままTを返す。
そうじゃないとき、T extends [infer R, ...infer U] を満たすならメインの型処理、そうでないなら、同じくそのままTを返す。

以下の記述についてみていく。

Flag extends false
  ? [R, ...Fill<U, N, Start, End, [...Count, 0]>]
  : [N, ...Fill<U, N, Start, End, [...Count, 0], Flag>]

ジェネリクスで以下のように記述がある。

  Flag extends boolean = Count['length'] extends Start ? true : false

Countに内容が格納されていく段階で、Count['length']がStartを超えた場合にFlagをたてている。
ジェネリクスを利用したこういうテクニックもあるのか。

Flagがたっていない状態だと、

[R, ...Fill<U, N, Start, End, [...Count, 0]>]

R(inferで指定されているTの先頭要素、つまり処理中の要素)を代入し、
あとは、...Fill<inferの残り、Nは同じ、Startも同じ、Endも同じ、Count内に0を追加してカウントアップ>
といった処理をしている。

Flagがたっているばあい、

[N, ...Fill<U, N, Start, End, [...Count, 0], Flag>]

Nを格納するのとFlagを引き継ぐ以外は同じ。

一気に書くんじゃなく、問題で求められているのは配列型なんだから、[この中で型システムを利用] をまずは考えよう。
そして、ベースができた後に外側のConditional Typesを考えるという順序が筋が良さそう。

high-ghigh-g

Without

https://github.com/type-challenges/type-challenges/blob/main/questions/05117-medium-without/README.md

Arr[number]で値のユニオンを作るやつなはず。
書いてみたけど、テストケース通らない。

type Without<T extends any[], U> = {
  [K in keyof T as U extends any[]
    ? T[K] extends U[number] ? never : K
    : T[K] extends U ? never : K]: T[K]
}

解答を見る。

type ToUnion<T> = T extends any[] ? T[number] : T
type Without<T, U> = 
  T extends [infer R, ...infer F]
    ? R extends ToUnion<U>
      ? Without<F, U>
      : [R, ...Without<F, U>]
    : T

T extends [infer R, ...infer F] ? ◯ : Tは定型として良く出てくる。
R extends ToUnion<U> ? Without<F, U> の部分で、配列型のジェネリクスも一旦ユニオンでまとめ、
R(配列型内の精査対象の値)がユニオンに一致しないなら[R, ...Without<F, U>]として再帰処理。

解答を見てる限り、MappedTypes + Key Remappingでやってる人はいなさそう。
配列の再生成はinferを使うのか。

high-ghigh-g

IndexOf

https://github.com/type-challenges/type-challenges/blob/main/questions/05153-medium-indexof/README.md

配列を順番に精査するやつはinferを利用する。

一旦書いたやつ。

type IndexOf<T, U, Count extends number[] = []> = T extends [infer F, ...infer R]
  ? F extends U
    ? Count['length']
    : IndexOf<[...R], U, [...Count, 0]>
  : -1

リテラル型の対応はできているが、any, string, numberなどの広い型が対応できていない。
FとUを判定するConditional Typesをもう一つ追加。

type IndexOf<T, U, Count extends number[] = []> = T extends [infer F, ...infer R]
  ? F extends U
    ? U extends F 
      ? Count['length']
      : IndexOf<[...R], U, [...Count, 0]>
    : IndexOf<[...R], U, [...Count, 0]>
  : -1

anyの判定ができていない。
解答を見る。

type IndexOf<T extends any[], U, Pass extends any[] = []> = 
  T extends [infer F, ...infer Rest] 
    ? Equal<F, U> extends true
      ? Pass['length'] 
      : IndexOf<Rest, U, [...Pass, F]>
    : -1

Equalユーティリティを利用してるけど、これでいいの?
考え方は自分と同じ。

他の回答。

type IndexOf<T extends unknown[], U extends unknown, Count extends 1[] = []> =
  T extends [infer First, ...infer Rest] ? (
    (<V>() => V extends First ? 1 : 0) extends
    (<V>() => V extends U ? 1 : 0) ?
      Count['length'] : IndexOf<Rest, U, [...Count, 1]>
  ) : -1

Equalを自分で実装している人がいた。

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

初級か中級の最初の方でもあった気がする。
あとは、ConditionalTypesを()でひとまとめにすることもできるという学び。

high-ghigh-g

Join

https://github.com/type-challenges/type-challenges/blob/main/questions/05310-medium-join/README.md

一旦書いてみたやつでテストケースは通ったが、RやFの型解決が完璧ではない。

type Join<T extends string[], U extends string | number = ',', Res extends string = ''> = T extends [infer F, ...infer R]
  ? Res extends ''
    ? Join<R, U, F>
    : Join<R, U, `${Res}${U}${F}`>
  : Res

R のエラー
Type 'R' does not satisfy the constraint 'string[]'.
Type 'R[number]' is not assignable to type 'string'.

「R」の型は「string[]」の制約を満たしていません。 「R[number]」の型は「string」型に代入できません。

Fのエラー
Type 'F' is not assignable to type 'string | number | bigint | boolean | null | undefined'.(2322)

不細工になったけど、とりあえずクリア。

type Join<T extends string[], U extends string | number = ',', Res extends string = ''> = T extends [infer F, ...infer R]
  ? F extends string
    ? R extends string[]
      ? Res extends ''
        ? Join<R, U, F>
        : Join<R, U, `${Res}${U}${F}`>
      : never
    : never
  : Res

解答を見る。

type Join<T extends any[], U extends string | number> = T extends [infer F, ...infer R]
  ? R['length'] extends 0
    ? `${F & string}`
    : `${F & string}${U}${Join<R, U>}`
  : never

型システム内でキャストっぽいことできたのがうっすら記憶にあったけど、&stringかー。
保持する為のジェネリクスを持たずに、文字列を連結していけばいいのもシンプルな記述でいい。

別解

type Join<T extends string[], U extends string | number> = 
  T extends [infer L extends string, ...infer R extends string[]]
    ? R['length'] extends 0
      ?  L
      : `${L}${U}${Join<R, U>}`
    : ''

infer にextendsをつけることで、制約として示せるのもあったなあ。。

自分の解答を修正して、ちょっとシンプルになった。

type Join<T, U extends string | number = ',', Res extends string = ''> = T extends [infer F extends string, ...infer R extends string[]]
  ? Join<R, U, Res extends '' ? `${F}` : `${Res}${U}${F}`> : Res
high-ghigh-g

LastIndexOf

https://github.com/type-challenges/type-challenges/blob/main/questions/05317-medium-lastindexof/README.md

IndexOfと逆で最後のパスを記述
とりあえず書いてみた。

type LastIndexOf<T extends any[], U, Count extends number[] = [], Index extends number = 0> =
  T extends [infer F, ...infer Rest]
    ? LastIndexOf<Rest, U, [...Count, 1],  Equal<F, U> extends true ? Count['length'] : Index>
    : Index

以下のテストケースが満たせていない。
Expect<Equal<LastIndexOf<[0, 0, 0], 2>, -1>>,

ちょっと不細工だけど、Flgのジェネリクスを作成してクリア。

type LastIndexOf<T extends any[], U, Count extends number[] = [], Index extends number = 0, Flg extends boolean = false> =
  T extends [infer F, ...infer Rest]
    ? LastIndexOf<Rest, U, [...Count, 1], Equal<F, U> extends true ? Count['length'] : Index, Flg extends false ? Equal<F, U> : Flg>
    : Flg extends true ? Index : -1

解答を見る。

めちゃくちゃシンプル。

type LastIndexOf<T extends any[], U> = T extends [...infer I,infer L]? L extends U?I['length']: LastIndexOf<I,U> : -1;

なるほど逆からみていくのか。
LがUに代入できるときに、Iのlengthを返す、そうでないなら再起、全体に当てはまらないなら-1。

type LastIndexOf<T, U> = T extends [...infer Rest, infer F] 
  ? Equal<F, U> extends true
    ? Rest['length'] 
    : LastIndexOf<Rest, U>
  : -1

この人も逆から見てる。
かつ、Equalの判定を先に入れてる。最もシンプルな解答と大体の構成が同じ。
勉強になる。

high-ghigh-g

Unique

https://github.com/type-challenges/type-challenges/blob/main/questions/05360-medium-unique/README.md

一旦こういうのを書いて、スタックしていく作りにしたい。

type Unique<T extends any[], Stack extends any[] = []> = T extends [infer First, ...infer Rest]
  ? Unique<Rest, [...Stack, First]> : Stack

1つ目のConditional Typesの後にFirstがStack内にあるかないかの判定ができたらクリアできるかもしれん。

↓みたいなのを書いてみた。テストケース通らない。

type Unique<T extends any[], Stack extends any[] = []> = T extends [infer First, ...infer Rest]
  ? Unique<Rest, Stack extends [...infer _, First, ...infer __] ? [...Stack] : [...Stack, First]> 
  : Stack

詰まってる。解答を見る。

type Includes<T, U> = U extends [infer F, ...infer Rest] 
  ? Equal<F, T> extends true 
    ? true 
    : Includes<T, Rest> 
  : false;

type Unique<T, U extends any[] = []> = 
  T extends [infer R, ...infer F]
    ? Includes<R, U> extends true
      ? Unique<F, [...U]>
      : Unique<F, [...U, R]>
    : U

なるほど、やりたいことが出来なければ、自作ユーティリティでワンクッション挟む工夫をせねば。。

Equalで再帰比較してtrue, falseがわかればよいのか。簡単なロジックだった。。
全体としてやりたいことは似てるけど、ゴールにたどり着けなかったのは悔しい。

high-ghigh-g

MapTypes

https://github.com/type-challenges/type-challenges/blob/main/questions/05821-medium-maptypes/README.md

久しぶりにMappedTypesを利用する課題な気がする。

とりあえず書いた。

type MapTypes<T extends Record<string, any>, R extends {mapFrom: any, mapTo: any}> = {
  [K in keyof T]: T[K] extends R['mapFrom'] ? R['mapTo'] : T[K]
}

このテストケースが満たせていない。2つめのジェネリクスがユニオンの場合に分配が発生するとなにかおかしくなる。

Expect<Equal<MapTypes<{ name: string, date: Date }, { mapFrom: string, mapTo: boolean } | { mapFrom: Date, mapTo: string }>, { name: boolean, date: string }>>

こうなる
type Res = {
name: string | boolean;
date: string | boolean;
}

正しくは{ name: boolean, date: string }

解答を見る。

type MapTypes<T, R extends { mapFrom: any; mapTo: any }> = {
  [K in keyof T]: T[K] extends R['mapFrom']
  ? R extends { mapFrom: T[K] }
    ? R['mapTo']
    : never
  : T[K]
}

もう一段階、R extends {mapFrom: T[K]} ? R['mapTo'] : never のConditional Typesを挟むだけでいいのか。
Equal的な実装をしたい場合は、双方向で型が成り立つことを示すことが重要みたい。

high-ghigh-g

Number Range

https://github.com/type-challenges/type-challenges/blob/main/questions/08640-medium-number-range/README.md

インクリメント系の課題っぽい。とりあえず書いた。

type NumberRange<L extends number, H, Count extends number[] = []> =
  H extends Count['length'] ? Count[number] : NumberRange<[...Count, 1]['length'], H, [...Count, L]>

テストケースが通らない。lengthの判定が間違えてる。フラグを儲けよう。

type NumberRange<L extends number, H, Count extends number[] = [], Flg extends boolean = false> =
  Flg extends true
    ? Count[number]
    : H extends Count['length'] ? NumberRange<[...Count, 1]['length'], H, [...Count, L], true> : NumberRange<[...Count, 1]['length'], H, [...Count, L], Flg>

0始まりだったらクリアできてる。
Expect<Equal<NumberRange<2, 9>, Result1>>, がクリアできていない。
初期値の設定が悪い。
あー。。L < Count['length'] を判定するフラグもいる。。
クリア出来なさそう。解答を見る。

めちゃくちゃシンプルに解いてる人がいる。

type Utils<L, C extends any[] = [], R = L> = 
  C['length'] extends L
      ? R
      : Utils<L, [...C, 0], C['length'] | R>

type NumberRange<L, H> = L | Exclude<Utils<H>, Utils<L>>
  • NumberRangeが返す型はnumberリテラルユニオンにするので、基本は複雑なConditionalTypesを書かなくてもいい
  • ユニオンから特定の値を除外する場合はExclude(n回目の登場)
  • LとHで0始まりのnumberリテラルユニオンをそれぞれ作り、Excludeで被っている部分を除外

なるほど。Excludeの挙動が頭に入っていないと解けない課題。
なんでもかんでもジェネリクスに追加してクリアしようとする悪い癖が出た。

high-ghigh-g

Combination

https://github.com/type-challenges/type-challenges/blob/main/questions/08767-medium-combination/README.md

気づいたら5日間もtype-challengesやってない。。
勘所が鈍っているのとCombinationが解けずに詰まっているので、早速解答を見てみる。

type Combination<T extends string[], All = T[number], Item = All>
  = Item extends string
    ? Item | `${Item} ${Combination<[], Exclude<All, Item>>}`
    : never

考えていたものよりもかなりシンプル。
初期値でAllにT[number]で配列要素をユニオンで保持、Itemの中にAllと同じものを格納
Conditional TypesでItemがstringの場合に Item | ${Item} ${Combination<[], Exclude<All, Item>>} に分岐
${Item} ${Combination<[], Exclude<All, Item>>} の部分で分配されていい感じに連結されるんかな?
二度目の再帰以降は、Tは空配列、ジェネリクスの2つ目にExclude<All, Item>。ここで分配が発生してそう。
分配の挙動イメージが分かりにくい。。
生成AIに聞いてみる。

// この場合
type Example = Combination<['A', 'B', 'C']>;

// 最初の呼び出し
Item = 'A'
All = 'A' | 'B' | 'C'
// 結果の一部: 
'A' | `A ${Combination<[], 'B' | 'C'>}`

// Combination<[], 'B' | 'C'> の展開:
Item = 'B'
All = 'B' | 'C'
'B' | `B ${Combination<[], 'C'>}`

// Combination<[], 'C'> の展開:
Item = 'C'
All = 'C'

// 結果の一部:
'C' | `C ${Combination<[], never>}`

順番に書き出していくの大事。
分配系の問題はそれだけで何度も繰り返した方がよさそう。

high-ghigh-g

Subsequence

https://github.com/type-challenges/type-challenges/blob/main/questions/08987-medium-subsequence/README.md

とりあえず思いついたやつ。

type Subsequence<T extends any[], Res = T | []> =  T extends [infer F, ...infer Rest] ? Subsequence<Rest, Res | [F]> : Res

ユニオンを増やしていく系も他のジェネリクスを使う課題と変わらず考えればいいことを学んだ。
ただし、、以下のテストケースしかクリアできていない。
Expect<Equal<Subsequence<[1, 2]>, [] | [1] | [2] | [1, 2]>>
Expect<Equal<Subsequence<['x', 'y']>, [] | ['x'] | ['y'] | ['x', 'y'] >>,

例えば、type res = Subsequence<[1, 2, 3, 4, 5]>の場合に、[1, 2] | [1, 3] | [1, 4] | [1, 5] | [2, 3] | [2, 4] | [2, 5] のような分配が出来ていない。
直近の課題と同じ。
あれ、直近の文字列を作るパターンがそのまま配列型に置き換わっただけのように見える。

時間がないので解答を見る

type Subsequence<T extends any[], Prefix extends any[] = []> =
  T extends [infer F, ...infer R]
    ? Subsequence<R, Prefix | [...Prefix, F]>
    : Prefix

Prefixのジェネリクスに対してPrefix | [...Prefix, F]で記述するのもありなのか。
とりあえず写経した。手で覚えよう。。
まだユニオンと仲良くなれてない。

他の回答

type Subsequence<T extends unknown[]> = T extends [infer X, ...infer Y]
  ? [X, ...Subsequence<Y>] | Subsequence<Y>
  : [];

こっちは更にシンプルだし、センスある。。
構造をちゃんと理解しないといけない。[X, ...Subsequence<Y>] | Subsequence<Y>
明らかに分配が発生している問題がむずい。

high-ghigh-g

CheckRepeatedChars

https://github.com/type-challenges/type-challenges/blob/main/questions/09142-medium-checkrepeatedchars/README.md

一旦書いてみた。

type CheckRepeatedChars<T extends string, C extends String = ''> =
  T extends `${infer F}${infer R}`
    ? F extends C
      ? true
      : CheckRepeatedChars<R, F>
    : false

常に隣同士の値との判定しかできていない。
Expect<Equal<CheckRepeatedChars<'cbc'>, true>>,のテストケースが満たせていない。
ループが回りきってからCheckStrに入る状態を変更する形にする必要がある。

最後まで処理が走りきった場合の判定をどうするか

  1. 1文字目を取得
  2. 1文字目をスタックし、Tを分解しながら、各文字を比較
  3. 比較途中で一致したらその時点でtrueを返す
  4. 文字が一致しない場合、スタックを空文字にし、CheckRepeatedCharsの1文字目を全文にして切り替え?

多分、再帰の書き方が悪い。
解答を見る。

type CheckRepeatedChars<T extends string> = T extends `${infer F}${infer E}` 
  ? E extends `${string}${F}${string}`
    ? true
    : CheckRepeatedChars<E>
  : false

賢いすぎる。。
一文字目とそれ以外に分けて、それ以外の文字列型がstring F stringの型と一致するなら、trueそうでなければ再帰、文字比較がなくなりしだいfalse。

2つ目。

type CheckRepeatedChars<T extends string, U = never> = T extends `${infer P}${infer R}`
? P extends U 
  ? true
  : CheckRepeatedChars<R, U | P>
: false

こっちも賢い。ジェネリクスUに再帰を回しながらリテラルユニオンを格納していき、
P が Uに代入できるなら、trueとする。

型が一致しているかを確認する方法として、2種類の方法を利用している。

  • あくまで全文の型が一致しているか
  • 一文字ずつユニオンとしてスタックしたものを確認するか

賢い。

high-ghigh-g

FirstUniqueCharIndex

https://github.com/type-challenges/type-challenges/blob/main/questions/09286-medium-firstuniquecharindex/README.md

書いてみた。

type FirstUniqueCharIndex<T extends string, Count extends number[] = []> = T extends `${infer F}${infer R}`
  ? R extends `${string}${F}${string}`
    ? FirstUniqueCharIndex<R, [...Count, 1]>
    : R extends '' ? -1 : Count['length']
  : -1

Expect<Equal<FirstUniqueCharIndex<'aabb'>, -1>>,
のテストケースが満たせていない。

Tがabbとなった時に、Fがa、Rがbbとなった時の対応ができていない。

わからなくなってきた。。

type FirstUniqueCharIndex<T extends string, Count extends number[] = []> = T extends `${infer F}${infer R}`
  ? R extends `${string}${F}${string}`
    ? FirstUniqueCharIndex<R, [...Count, 1]>
    : T extends `${string}${F}`
      ? FirstUniqueCharIndex<R, [...Count, 1]>
      : Count['length'] extends T['length']
        ? Count['length']
        : FirstUniqueCharIndex<R, [...Count, 1]>
  : -1

解答を見る。

type FirstUniqueCharIndex<
  T extends string,
  _Acc extends string[] = []
> = T extends ''
  ? -1
  : T extends `${infer Head}${infer Rest}`
  ? Head extends _Acc[number]
    ? FirstUniqueCharIndex<Rest, [..._Acc, Head]>
    : Rest extends `${string}${Head}${string}`
    ? FirstUniqueCharIndex<Rest, [..._Acc, Head]>
    : _Acc['length']
  : never

空文字の場合はカッコつけずに最初に判定する。
いつもの、T extends ${infer Head}${infer Rest}
HEAD extends _Acc[number] で ユニオンの中に文字列が存在していないかで確認してるのか。賢い。
ユニオンで判定するテクニックは実用していきたい。
ユニオンの中で既に一致する文字があった場合は、再帰
そうじゃない場合、Rest extends ${string}${Head}${string} 先頭の一文字と残りの文字列を比べる。
ここでも一致するなら再帰。
ここまでの処理で一致する文字がない場合にその時の_Acc['length']を型とする。
問題の切り分け方がうまい。

type FirstUniqueCharIndex<T extends string, N extends string = T, R extends unknown[] = []> = T extends '' ? -1 :
    (N extends `${infer F1}${infer S}` ?
        (T extends `${string}${F1}${string}${F1}${string}` ? FirstUniqueCharIndex<T, S, [...R, unknown]> : R['length']) : -1
    );

空文字チェックとN extends ${infer F1}${infer S} ?の辺りは同じ。
T extends ${string}${F1}${string}${F1}${string} これ賢い。一発で2つ以上の文字の確認が出来ている。
シンプル。

high-ghigh-g

Parse URL Params

https://github.com/type-challenges/type-challenges/blob/main/questions/09616-medium-parse-url-params/README.md

解けた。多分大体のパターンに対応出来ているのではと思う。ユニオン

type ParseUrlParams<T extends string> =
  T extends `${infer _}:${infer Param}`
    ? Param extends `${infer Id}/${infer Param2}`
      ? Id | ParseUrlParams<Param2>
      : Param
    : never

解答を見る。

ひとつめ

type ParseUrlParams<T> = T extends `${string}:${infer R}`
  ? R extends `${infer P}/${infer L}`
    ? P | ParseUrlParams<L>
    : R
  : never

お!すごい。今回書いた記述とほぼ同じ。

2つ目

type ParseUrlParams<
  T extends string,
  R extends string = never
> = 
T extends `${string}:${infer S}/${infer L}`?
  ParseUrlParams<L,R | S>:
  T extends `${string}:${infer S}`?
    R | S
    : R

ユニオン部分をジェネリクスに格納する形。これもいい。

high-ghigh-g

GetMiddleElement

https://github.com/type-challenges/type-challenges/blob/main/questions/09896-medium-get-middle-element/README.md

シンプルだけどどう解けばいいかわからない。
型システムで半分ってどう判定するんだろう。
ちょっと取っ掛かりが見えなさすぎるので、解答を早速見る。

1つ目

type GetMiddleElement<T extends any[]> = 
  T['length'] extends 0 | 1 | 2?
    T:
    T extends [any,...infer M,any]?
      GetMiddleElement<M>:never

第一印象。なにこれ、めちゃくちゃシンプル。
最終的にほしい配列型の長さが、0か1か2のパターンしかないため、 0 | 1 | 2との比較した条件型。(賢すぎる。)
0か1か2なら即Tを返す。
そうでない場合、[any,...infer M,any]との条件型を取って、再帰。

型引数が、[1,2,3,4,5]のとき、
[1,2,3,4,5] → [2,3,4] → [3] と処理される。
頭良すぎる。。

2つ目

type GetMiddleElement<T extends any[]> = T extends [infer L, ...infer M, infer R]
    ? M extends []
        ? [L, R]
        : GetMiddleElement<M>
    : T

少しアプローチは異なっているが、配列の先頭と末尾があるかどうかでまず判定し、次にMの状態で判定している。

型システムの場合、半分を計算するにはどうするかの様な正攻法を考えたらいけない。。

high-ghigh-g

Appear only once

https://github.com/type-challenges/type-challenges/blob/main/questions/09898-medium-zhao-chu-mu-biao-shu-zu-zhong-zhi-chu-xian-guo-yi-ci-de-yuan-su/README.md

堅実に条件をかき分けた。

type FindEles<T extends any[], Stack extends unknown[] = [], Res extends unknown[] = []> =
  T extends [infer F, ...infer Rest]
    ? F extends Rest[number]
      ? F extends Stack[number]
        ? FindEles<Rest, Stack, Res>
        : FindEles<Rest, [...Stack, F], Res>
      : F extends Stack[number]
        ? FindEles<Rest, Stack, Res>
        : FindEles<Rest, [...Stack, F], [...Res, F]>
    : Res

以下のテストケースが判定できていない。ユニオンでの判定がただの横着になってる。
Expect<Equal<FindEles<[1, 2, number]>, [1, 2, number]>>,
Expect<Equal<FindEles<[1, 2, number, number]>, [1, 2]>>,

厳密な型チェックが難しい。
解答を見る。

1つ目

type Includes<T extends unknown[], Target extends unknown> = T extends [
  infer Head,
  ...infer Tail,
]
  ? Equal<Head, Target> extends true
    ? true
    : Includes<Tail, Target>
  : false;

type FindEles<
  T extends unknown[],
  Duplicates extends unknown[] = [],
  Unique extends unknown[] = [],
> = T extends [infer Head, ...infer Tail]
  ? Includes<Tail, Head> extends true
    ? FindEles<Tail, [...Duplicates, Head], Unique>
    : Includes<Duplicates, Head> extends true
      ? FindEles<Tail, Duplicates, Unique>
      : FindEles<Tail, Duplicates, [...Unique, Head]>
  : Unique;

愚直にnumberのチェックを確認する処理をいれてる。
このIncludes型結構便利。

high-ghigh-g

Count Element Number To Object

https://github.com/type-challenges/type-challenges/blob/main/questions/09989-medium-tong-ji-shu-zu-zhong-de-yuan-su-ge-shu/README.md

休み明けに久しぶりにtype-challengesをやる。。
とりあえず1回分だけカウントするやつ

type CountElementNumberToObject<T extends unknown[], Stack extends unknown[] = []> = {
  [K in keyof T as T[K]]: 1
}

カウントユーティリティを作らねば・・・・。

わからんから解答を見る。
https://github.com/type-challenges/type-challenges/issues/28355

type Flatten<T,R extends any[] = []> = 
  T extends [infer F,...infer L]?
    [F] extends [never]?
      Flatten<L,R>:
      F extends any[]?
        Flatten<L,[...R,...Flatten<F>]  >
        :Flatten<L,[...R,F]>
    :R 


type Count<T, R extends Record<string | number,any[]> = {}> = 
T extends [infer F extends string | number,...infer L]?
  F extends keyof R?
    Count<L, Omit<R,F>& Record<F,[...R[F],0] > >
    : Count<L, R & Record<F,[0]>>
  :{
    [K in keyof R]:R[K]['length']
  }


type CountElementNumberToObject<T> = Count<Flatten<T>>

Flattenで1次配列化
CountでConditionalTypes + inferを使って中身を展開しながらmapped typesで最後カウント

high-ghigh-g

Integer

https://github.com/type-challenges/type-challenges/blob/main/questions/10969-medium-integer/README.md

とりあえず小数点部分を考慮したやつ

type Integer<T extends number> = `${T}` extends `${number}.${number}` ? never : T

リテラルかどうかってどうチェックするんだろう。
解答見る。

type Integer<T extends number> = `${T}` extends `${bigint}` ? T : never

bigintという型が出てきた。
https://typescriptbook.jp/reference/values-types-variables/bigint

なるほど。トリッキー。
number型を避けつつ、リテラルをそのまま扱えている。

2つ目の解答

type Integer<T extends string | number> = number extends T
  ? never
  : `${T}` extends `${string}.${string}`
    ? never
    : T

こっちが本筋な気がする。
number extends T ? neverをさきに書いてしまうのか。

high-ghigh-g

ToPrimitive

https://github.com/type-challenges/type-challenges/blob/main/questions/16259-medium-to-primitive/README.md

初見殺し系だ。リテラルからのプリミティブ何も出来ない。
とりあえずMappedTypesだけ書いて解答を見る。

type ToPrimitive<T> = {
  [K in keyof T]: T[K]
}

解答を見る。

1つめ。愚直に書いてる。なるほどこれでいいのか。

type ToPrimitive<T> = T extends string
  ? string
  : T extends number
  ? number
  : T extends boolean
  ? boolean
  : T extends bigint
  ? bigint
  : T extends symbol
  ? symbol
  : {
      [K in keyof T]: ToPrimitive<T[K]>
    }

Functionの場合の変換が出来ていなかったので少し整理して書き直してみた。

type ToPrimitive<T> =
  T extends string ? string
  : T extends number ? number
  : T extends boolean ? boolean
  : T extends symbol ? symbol
  : T extends Function ? Function
  : { [K in keyof T]: ToPrimitive<T[K]> }

2つ目。よりシンプル。

type ToPrimitive<T>
  = T extends Function ? Function
  : T extends object ? { [K in keyof T]: ToPrimitive<T[K]> }
  : T extends { valueOf(): infer R } ? R
  : T

valueOf()を使って T extends { valueOf(): infer R } ? R : never こういう実装ができるの知らなかった。
これでリテラル型のプリミティブ型を作成できる。

high-ghigh-g

DeepMutable

https://github.com/type-challenges/type-challenges/blob/main/questions/17973-medium-deepmutable/README.md
ほぼ2週間ぶり・・・。

書いてみた。

type DeepMutable<T> = {
  -readonly [K in keyof T]: T[K]
}

入れ子のデータが解決出来ていないため追加。1つ目のテストケースはクリア。

type DeepMutable<T> = {
  -readonly [K in keyof T]: T[K] extends Record<string, any> ? DeepMutable<T[K]> : T[K]
}

分からない。解答を見る。

type DeepMutable<T extends Record<keyof any,any>> =
  T extends (...args:any[])=>any?
    T:
    { - readonly [K in keyof T]:DeepMutable<T[K]> }

関数型かどうかが考慮できてたら良かったみたい。

同じく疑問に思ったことが書かれてある。

DeepMutable<T[K]>なぜ直接使用できるのですか?T[k]はプリミティブになる可能性があり、なぜジェネリック制約をトリガーしないのですかT extends Record<keyof any, any>?

https://github.com/type-challenges/type-challenges/issues/18259#issuecomment-1727145167

ほー(わかったようなわからないような。とにかくMappedTypesの再帰でプリミティブ型が発生した場合、再帰の処理を通らずに自動的にプリミティブ型が返されるとのこと)

type DeepReadonly<T> = 
  { readonly [K in keyof T]: DeepReadonly<T[K]> };
マップされた型は、入力を返すことによってすでにプリミティブを「スキップ」しており、自動的にユニオンに分配されるため、これらを自分で確認する必要はありません。

type Look<T> = { [K in keyof T]: 123 };
type Y1 = Look<{ a: string }> // {a: 123}
type Y2 = Look<string> // string
type Y3 = Look<{ a: string } | { b: string }>
//  Look<{ a: string; }> | Look<{ b: string; }>
high-ghigh-g

Filter

https://github.com/type-challenges/type-challenges/blob/main/questions/18220-medium-filter/README.md

クリア。急に難易度が優しくなった。

type Filter<T extends any[], P, Result extends any[] = []> =
  T extends [infer First, ...infer Other]
    ? First extends P
      ? Filter<Other, P, [...Result, First]>
      : Filter<Other, P, Result>
    : Result

解答を見る。

type Filter<T extends unknown[], P> = T extends [infer F, ...infer R]
  ? F extends P
    ? [F, ...Filter<R, P>]
    : Filter<R, P>
  : [];

シンプルでよき。
配列型を返す場合、ジェネリクスを改めて作らなくても配列型の中で再帰を書けばいいのか。

high-ghigh-g

FindAll

https://github.com/type-challenges/type-challenges/blob/main/questions/21104-medium-findall/README.md

書いてみた。通らない。

type FindAll<T extends string, P extends string, Res extends number[] = []> = 
  T extends `${infer First}${P}${infer Other}` ? FindAll<Other, P, [...Res, First['length']]> : Res

日が経ってるから単純なカウントの方法を忘れてる。
テンプレートリテラルにPが含まれているかどうかを精査し、一致した場合の前半のlengthを取得すればいいと思ってたけど、通らない。

解答を見てみる。

もっともシンプルそうな解答。

type FindAll<
  T extends string,
  P extends string,
  L extends 0[] = [],
> = P extends ''
  ? []
  : T extends `${string}${infer R}`
    ? [
        ...(T extends `${P}${string}` ? [L['length']] : []),
        ...FindAll<R, P, [0, ...L]>,
      ]
    : []

ジェネリクス制約でextends 0[]とした場合、0のみが含まれる配列型を定義できるのか。
Pが空文字の場合のチェック→2文字以上かのチェック→配列型内でConditionalTypesの展開。
配列型の中で記述されている...FindAll<R, P, [0, ...L]>,の方でカウントアップしながら、Tから文字列を削っていく→、T extends ${P}${string} に該当したときの、Lのlengthを追加、そうでなければから配列とする。
なるほど。。賢い。
https://github.com/type-challenges/type-challenges/issues/31533

他の解答も同じ様に元の文字列を1文字ずつ先頭から削りながらカウントアップしてる。
T extends ${string}${infer L}? T extends ${S}${string}? の組み合わせ + 配列型のカウントアップ + 配列型['length'] が定石ぽく感じる。

high-ghigh-g

Combination key type

https://github.com/type-challenges/type-challenges/blob/main/questions/21106-medium-zu-he-jian-lei-xing-combination-key-type/README.md

Distribution Conditional Typesの挙動が頭に入っていないので、早速解答を見る。

type Combs<T extends string[] = []> = T extends [infer Left extends string, ...infer Rest extends string[]] ? `${Left} ${Rest[number]}` | Combs<Rest> : never

${Left} ${Rest[number]} の記述で、分配的な挙動が発生し、
type ModifierKeys = ['cmd', 'ctrl', 'opt', 'fn'] が展開され、
"cmd ctrl" | "cmd opt" | "cmd fn" となる。

${Left} ${Rest[number]} | Combs<Rest> と記述することで、
再帰的にUnion Distributionとして定義され、テストケースを満たすことができる。

arr[number] = 配列型をユニオンで列挙する = Union Distiobutionとして覚えよう。

high-ghigh-g

Permutations of Tuple

https://github.com/type-challenges/type-challenges/blob/main/questions/21220-medium-permutations-of-tuple/README.md

与えられた配列型をの並び順を変更し、ユニオンで全パターンを網羅する内容ぽい。
またしてもDistribution...

雰囲気書いてみた。こういうことを表現したい。

type PermutationsOfTuple<T extends unknown[]> = T | T extends [infer First, ...infer Other] ? [First] | PermutationsOfTuple<[...Other, First]> : []

Distribution系はしばらく解答を見て慣れよう。

解答1つ目

type Insert<T extends unknown[], U> =
  T extends [infer F, ...infer L] ? [F, U, ...L] | [F, ...Insert<L, U> ] : [U]

type PermutationsOfTuple<T extends unknown[], R extends unknown[] = []> = 
  T extends [infer F,...infer L] ? PermutationsOfTuple<L, Insert<R, F> | [F, ...R] > : R

↓このユーティリティ型を考えられるかどうかが重要。
type Insert<T extends unknown[], U> =
T extends [infer F, ...infer L] ? [F, U, ...L] | [F, ...Insert<L, U> ] : [U]

配列型Tの先頭から2番目の所にUを差し込む&L
問題を分離すれば、ユーティリティ型を作るべきという発想になると思ったけど、RがユニオンになるからそもそもDistibutionの挙動が理解できていないとむずい。。

本格的にDistirbutionの挙動を理解しよう。。

high-ghigh-g

Replace First

https://github.com/type-challenges/type-challenges/blob/main/questions/25170-medium-replace-first/README.md

できた。感覚的にこれ系のやつはもっとシンプルに記述できる。

type ReplaceFirst<T extends readonly unknown[], S, R, Stack extends unknown[] = []> = 
  T extends [infer First, ...infer Other]
    ? First extends S 
      ? [...Stack, R, ...Other]
      : ReplaceFirst<Other, S, R, [...Stack, First]>
    : Stack 

解答を見る。
1つ目。

type ReplaceFirst<T extends readonly unknown[], S, R> = T extends readonly [infer F, ...infer Rest] ? F extends S ? [R, ...Rest] : [F, ...ReplaceFirst<Rest, S, R>] : []

新しいジェネリクスを利用せず、[F, ...ReplaceFirst<Rest, S, R>] で表現できる。
他は自分と同じ様な解答だった。

high-ghigh-g

Transpose

https://github.com/type-challenges/type-challenges/blob/main/questions/25270-medium-transpose/README.md

テストケースを見て動きのイメージを作れなかったけど、行列の転置というものをするみたい。

行列の転置とは、行列を対角線に沿って反転させる演算のことです。つまり、行列Aの行と列の添え字を入れ替えて、通常A<sup>T</sup>と表記される別の行列を生成することです。

[[1]] の様な配列は number[][] で表現するみたい。初めて知った。

そもそも転置行列を知らないから調べてみた。
https://manabitimes.jp/math/1046

ある行列の縦横を入れ替えた行列のことみたい。問題文の意味が分かった。
が、しかし、、これはむずい。。

とっかかりとして、空配列周りの記述、あとは、中身がある場合に M extends [infer First extends number[], ...infer Rest extends number[][]] で分解していく形。

type Transpose<M extends number[][], Result extends number[][] = []> = M extends [] 
  ? [] 
  : M[0] extends undefined 
    ? []
    : M extends [infer First extends number[], ...infer Rest extends number[][]]
      ? Result // ここに再帰を記述していくイメージ
      : [];

0番目はそのまま、0,1は1,0・・・。
for文で書くようなコードをinfer + 再帰で記述する場合、どうするか。
配列内の座標を取得する方法が配列型のlengthでしか無理そう。

んーちょっと今の自分の力では1時間内に解けない。解答を見る。

1つ目。

type Transpose<M extends number[][],R = M['length'] extends 0?[]:M[0]> = {
  [X in keyof R]:{
    [Y in keyof M]:X extends keyof M[Y]?M[Y][X]:never
  }
}

シンプルで芸術点がかなり高い。
ジェネリクスの初期値にConditional Typesを使えるのが初めて知った。
R = M['length'] extends 0?[]:M[0] Mのlengthが0なら、空配列、そうでないならMの0番目の列を代入している。
0番目が代入できていれば、あとはMappedTypesを利用すれば良いのか。。
解答を見たら簡単そうに見えるけど、Mapped Typesを使って、XとYを網羅的に見る記述が浮かばない。
今回の問題の場合、XとYはあらかじめ決まっているから
[X in keyof R]でX軸の値を展開し、
[Y in keyof M]:X extends keyof M[Y]?M[Y][X]:neverでY軸の値を展開しつつ、M[Y][X]でXとYの転置を行っている。

他の解答の場合だと、行列を転置させるユーティリティ型を作成されていた。

type Col<T extends number[][], I extends number> = T extends [infer F extends number[], ...infer R extends number[][]] ? [F[I], ...Col<R, I>] : [];

type Transpose<M extends number[][], _Result extends number[][] = []> = _Result[`length`] extends M[0][`length`] ?
  _Result/*return*/ : Transpose<M, [..._Result, Col<M, _Result[`length`]>]>;