Open137

type-challengesをやってみる

NozomuNozomu
NozomuNozomu

メモ

この問題はHelloWorld型を変えて、以下の型のエラーを直すこと

type HelloWorld = any
type test = Expect<Equal<HelloWorld, string>> // エラー

type HelloWorld = anyはHelloWorldという名前を持つ新しい型(型エイリアス)を作っている。
HelloWorld型はany型と同等になっているが、HelloWorld型とstring型と同じになることが求められているため、type HelloWorld = stringにして、string型と同等にすればいい

型エイリアス

TypeScriptでは、型に名前をつけられます。名前のついた型を型エイリアス(タイプエイリアス; type alias)と呼びます。

https://typescriptbook.jp/reference/values-types-variables/type-alias

NozomuNozomu

おまけ

この問題で使っている型を見てみる

Expect型

https://github.com/type-challenges/type-challenges/blob/main/utils/index.d.ts#L1

extendsを使って、Expectの渡されるジェネリクスの型Tをtrueに限定している→true以外が渡されると型エラーが起きる。

Equal型

https://github.com/type-challenges/type-challenges/blob/main/utils/index.d.ts#L7-L9

conditional typesを使っていて、(<T>() => T extends X ? 1 : 2)(<T>() => T extends Y ? 1 : 2)が同じ型ならtrue、違う場合はfalseを返す。
どうやらConditional Type内の<T>() => Tが決まらず、遅延評価されるので(<T>() => T extends X ? 1 : 2) extends(<T>() => T extends Y ? 1 : 2)のまま比較されるみたい。
それでなんやかんやあって、isTypeIdenticalToというtypescriptの内部的な実装が===を用いて厳密な等価評価をしてくれる感じ?

type Equal<X, Y> = X extends Y ? true : falseでもいけるのでは」と思ったがEqual<true, boolean>とかも通っちゃうのでダメだった残念

参考にさせていただいた記事
https://zenn.dev/razokulover/articles/890102685d5ea2
https://zenn.dev/yumemi_inc/articles/ff981be751d26c#まとめ:仕組みの説明

ジェネリクス

型の引数のようなもの。型の安全性とコードの共通化を両立できる。
https://typescriptbook.jp/reference/generics

extends

TypeScriptではextendsキーワードを用いることでジェネリクスの型Tを特定の型に限定することができます。

https://typescriptbook.jp/reference/generics/type-parameter-constraint#型引数に制約をつける

conditional types

型の三項演算子みたいなイメージで、extendsで制限した型になっているかどうかで返す値を変える。
https://www.typescriptlang.org/docs/handbook/2/conditional-types.html

NozomuNozomu
NozomuNozomu

メモ

課題は組み込みの型ユーティリティPick<T, K>を使用せず、TからKのプロパティを抽出する型を実装すること

まず、PickのKにはTで与えたオブジェクトの型のkeyを渡すのでkeyofを使って、K extends keyof Tと表せる。

- type MyPick<T, K> = any
+ type MyPick<T, K extends keyof T> = any

またKはunion型で渡されるので、mapped typesが使えそう。
なのでこんな感じ↓

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

keyof

keyofはオブジェクトの型からプロパティ名を型として返す型演算子です。
https://typescriptbook.jp/reference/type-reuse/keyof-type-operator

インデックス型

オブジェクトのフィールド名をあえて指定せず、プロパティのみを指定したい場合に使う。
フィールド名の型はstring、number、symbolのみが指定できる。
型変数はKやkeyにするのが一般的。

let obj: {
  [K: string]: number;
};

https://typescriptbook.jp/reference/values-types-variables/object/index-signature

ユニオン型

TypeScriptのユニオン型(union type)は「いずれかの型」を表現するものです。
ユニオン型の型注釈は、2つ以上の型をパイプ記号(|)で繋げて書きます。

https://typescriptbook.jp/reference/values-types-variables/union

Mapped Types

ユニオン型と組み合わせて使い、インデックス型と同じようにキーの制約として使用することができる。インデックス型と異なり、追加のプロパティが定義できない。
Mapped Typesに現れるinはユニオン型を反復処理するために使用される。

type MappedType = {
  [key in "foo" | "bar"]: string;
};

https://typescriptbook.jp/reference/type-reuse/mapped-types

NozomuNozomu

回答

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

おまけ

MyPick<T, K>の部分を変えずにasを使って実装しているのがあった、面白い!

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

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

でも適当なstringを与えても通っちゃうっぽい😇

型アサーション「as」(type assertion)

TypeScriptには、型推論を上書きする機能があります。その機能を型アサーション(type assertion)と言います。

https://typescriptbook.jp/reference/values-types-variables/type-assertion-as

NozomuNozomu
NozomuNozomu

メモ

組み込みの型ユーティリティReadonly<T>を使用せず、Tのすべてのプロパティを読み取り専用にする型を実装すること

type MyReadonly<T> = any

readonlyが使えそう。
Tのkeyをkeyofでユニオン型にして、Mapped Typesを使えばいけそう

- type MyReadonly<T> = any
+ type MyReadonly<T> = { readonly [K in keyof T]: T[K] }

readonly

TypeScriptの型システムでは、インターフェース上の個々のプロパティをreadonlyとしてマークすることができる。これにより、予期せぬ変更を防げる。
https://typescript-jp.gitbook.io/deep-dive/type-system/readonly

NozomuNozomu
NozomuNozomu

メモ

課題はタプルを受け取り、その各値のkey/valueを持つオブジェクトの型に変換する型を実装すること

これもMapped Typesを使って、一旦こんな感じ↓

- type TupleToObject<T extends readonly any[]> = any
+ type TupleToObject<T extends readonly any[]> = { [K in keyof T]: T[K] }

でもこれだと[K in keyof T]の部分が配列のkeyになってしまうので、valueにしたい

配列の型[number]で配列のvalueをユニオン型で取得できるのか!
https://typescriptbook.jp/tips/generates-type-from-array#すべてのリテラル型が欲しい

- type TupleToObject<T extends readonly any[]> = { [K in keyof T]: T[K] }
+ type TupleToObject<T extends readonly any[]> = { [K in T[number]]: K }

配列の型[number]を使って、valueでもKを使うようにして完成!

NozomuNozomu

回答

type TupleToObject<T extends readonly any[]> = { [K in T[number]]: K }
NozomuNozomu

おまけ

他の方の回答を見ると、Tの型を制限している。
確かにObjectの配列とかは対応できてないから制限したほうがいいなー

type TupleToObject<T extends readonly (string | symbol | number)[]> = {
  [P in T[number]]: P
}

symbol型

JavaScriptのsymbol型はプリミティブ型の一種で、その値が一意になる値です。boolean型やnumber型は値が同じであれば、等価比較がtrueになります。一方、シンボルはシンボル名が同じであっても、初期化した場所が違うとfalseになります。

あまり使う機会はなさそう

JavaScriptにシンボルが導入された動機は、JavaScriptの組み込みAPIの下位互換性を壊さずに新たなAPIを追加することでした。要するに、JavaScript本体をアップデートしやすくするために導入されたものです。したがって、アプリケーションを開発する場合に限っては、シンボルを駆使してコードを書く機会はそう多くはありません。

https://typescriptbook.jp/reference/values-types-variables/symbol

NozomuNozomu
NozomuNozomu

メモ

課題は配列Tを受け取り、その最初のプロパティの型を返すFirst<T>を実装すること

とりあえず、T[0]で最初のプロパティの型を返すようにした

- type First<T extends any[]> = any
+ type First<T extends any[]> = T[0]

[]を渡したときにneverを返すようにしたいのでconditional typesを使う

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

配列の1つの要素の型を取得する

https://typescriptbook.jp/tips/generates-type-from-array#何番目のリテラル型が欲しいか

NozomuNozomu

回答

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

おまけ

この問題結構色々なやり方してる人がいた

lengthがArrayのプロパティだから、T['length']みたいな書き方もできる感じかな?面白い

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

inferを使って配列の1つ目の要素の型を抽出している。inferで要素がなかったときだけnever型になるのを利用してておしゃれ

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

なるほど、T[number][]の時はneverになるのか

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

2個目のやつのunknownバージョン。改めてスプレッド構文的な感じで書けるんだー
anyじゃなくしてるのもいい感じ

type First<T extends unknown[]> = T extends [infer U, ...unknown[]] ? U : never

never型

TypeScriptのneverは「値を持たない」型。
neverへは何も代入できないが、neverは何にでも代入できる。
常に例外を起こす関数の戻り値に使える。
戻り値の意味として、voidは関数が最後まで実行されるという意味だが、neverは関数の処理が中断、もしくは、永遠に続くことを意味する。
https://typescriptbook.jp/reference/statements/never

infer

特定の型の一部を抽出し、それを型変数として取得するために使用される。
inferを使える場所はconditional typesのextendsの条件部分に限定される。
型を抽出できなかった場合はnever型になる。
https://zenn.dev/brachio_takumi/articles/464106a6a80eca8ab919#infer

型のスプレッド構文

配列の型が展開?される
https://zenn.dev/cookiegg/articles/typescript-spread-type

NozomuNozomu
NozomuNozomu

メモ

課題はタプルTを受け取り、そのタプルの長さを返す型Length<T>を実装すること

一個前の問題の回答であったT['length']でそのままいけそう

- type Length<T> = any
+ type Length<T> = T['length']

型エラーでた。配列じゃない場合も考慮しなければ!
conditional typesで配列の時と配列じゃない時を分ける

- type Length<T> = T['length']
+ type Length<T> = T extends any[] ? T['length'] : 1

まだエラー出てる。as constを使ってるやつを渡してるから、readonlyが必要だった!

- type Length<T> = T extends any[] ? T['length'] : 1
+ type Length<T> = T extends readonly any[] ? T['length'] : 1

あ、逆にエラー出さなきゃいけなかったんだ

  // @ts-expect-error
  Length<5>,
  // @ts-expect-error
  Length<'hello world'>,

てことでextendsを前に移動

- type Length<T> = T extends readonly any[] ? T['length'] : 1
+ type Length<T extends readonly any[]> = T['length']

T['length']無しのは今の自分にはちょっと思いつかなかった

constアサーション

オブジェクトリテラルの末尾にas constを記述すればプロパティがreadonlyでリテラルタイプで指定した物と同等の扱いになります。
https://typescriptbook.jp/reference/values-types-variables/const-assertion

NozomuNozomu

おまけ

他の人の回答も見てみる

一応unknown使ってるやつ、unknown使えるならunknownのほうがいいかもなー

type Length<T extends readonly unknown[]> = T["length"];

inferでlengthのリテラル型出すやり方。これは思いつかなかった、inferまだ慣れてないなー

type Length<T extends any> = T extends { length : infer R } ? R : never;

上のやつ、正しくは以下になりそう。改めてみると配列のみに制限しているから、lengthは絶対存在するのに条件分岐するのあまりスマートじゃないな

type Length<T extends readonly unknown[]> = T extends { length : infer R } ? R : never;

リテラル型

TypeScriptではプリミティブ型の特定の値だけを代入可能にする型を表現できます。そのような型をリテラル型と呼びます。

https://typescriptbook.jp/reference/values-types-variables/literal-types

unknown型

型が何かわからないときに使う型安全なany型。
unknown型にはどのような値も代入できる。
any型はどのような型の変数にも代入できるが、unknown型の値は具体的な型へ代入できず、プロパティへのアクセス、メソッドの呼び出しも許されない。
https://typescriptbook.jp/reference/statements/unknown

NozomuNozomu
NozomuNozomu

メモ

課題は組み込みの型ユーティリティExclude <T, U>を使用せず、Uに割り当て可能な型をTから除外する型を実装すること

今までよりちょっとむずい感あるが、とりあえずUはTのどれかに制限する。

- type MyExclude<T, U> = any
+ type MyExclude<T, U extends T> = any

うーん、思いつかない
回答こんな感じだった。

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

Uを制限しないのもちょっとモヤモヤ感あるけどテストケース通ればいい感じか
T extends U ? never : TでUにないやつをTから取り除けるのかー
conditional typesの理解足りないかも

調べたらそれっぽいの出てきた↓

Distributive Conditional Types

conditional typesがジェネリクスに作用する際に、ユニオン型の場合は展開される的な感じかな?
T extends U ? never : TでTが'A' | 'B'、Uが'B'の時に('A' extends 'B' ? never : 'A') | ('B' extends 'B' ? never : 'B')になるみたいな
https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types

NozomuNozomu
NozomuNozomu

メモ

課題は、例えば:Promise<ExampleType>という型があった時にExampleTypeを取得する型を作る

inferを使って、例でいうExampleTypeの部分を取得できそう

- type MyAwaited<T> = any
+ type MyAwaited<T> = T extends Promise<infer R> ? R : never

ただこれだとPromise<Promise<Promise<string>>>とか多重のやつは対応できない
再帰関数みたいな感じでやれそうかも

- type MyAwaited<T> = T extends Promise<infer R> ? R : never
+ type MyAwaited<T> = T extends Promise<infer U> ? MyAwaited<U> : T

あとtype T = { then: (onfulfilled: (arg: number) => any) => any }にも対応しなきゃいけない

- type MyAwaited<T> = T extends Promise<infer U> ? MyAwaited<U> : T
+ type MyAwaited<T> = T extends Promise<infer R> ? R : never

普通のプログラミングのイメージに近づけると思いつきやすいなと感じた
めちゃくちゃ無理やりやった。

- type MyAwaited<T> = T extends Promise<infer R> ? R : never
+ type MyAwaited<T extends Promise<any> | ((arg: any) => any) | { [key: string]: any }> = 
+ T extends Promise<infer U> | ((arg: infer U) => any) 
+   ? U extends Promise<any> | ((arg: any) => any) 
+     ? MyAwaited<U> 
+   : U 
+ : T extends { [key: string]: infer U }
+   ?  U extends Function
+     ? MyAwaited<U> 
+   : T
+ : never

すごい普通にプログラム書いてる感覚で書けるようになってきたのは良かったが、絶対もっと簡単にかけるはず、、、

NozomuNozomu

回答

type MyAwaited<T extends Promise<any> | ((arg: any) => any) | { [key: string]: any }> = 
 T extends Promise<infer U> | ((arg: infer U) => any) 
   ? U extends Promise<any> | ((arg: any) => any) 
     ? MyAwaited<U> 
   : U 
 : T extends { [key: string]: infer U }
   ?  U extends Function
     ? MyAwaited<U> 
   : T
 : never
NozomuNozomu

おまけ

回答見てみたらtype T = { then: (onfulfilled: (arg: number) => any) => any }の条件をクリアしてるのが全然なかった。最近問題が変わったみたい?

組み込みのPromiseLikeというまさに{ then: (onfulfilled: ~みたいな感じの型があって、それを使ってるみたい。やっぱ組み込み便利だな〜

type MyAwaited<T extends PromiseLike<any | PromiseLike<any>>> =
  T extends PromiseLike<infer V>
    ? V extends PromiseLike<any>
      ? MyAwaited<V>
      : V
    : never

個人的にtypeを分けているのはいい感じで関数を細かく分けたりするのに近いものを感じる。真似していきたい。ただ、Thenableの汎用性が低いのがちょっと微妙かも?
then: (onfulfilled: (arg: T) => unknown) => unknownを完全に別物としてやるのを思いつかなかった

type Thenable<T> = {
  then: (onfulfilled: (arg: T) => unknown) => unknown;
}

type MyAwaited<T extends Thenable<any> | Promise<any>> = T extends Promise<infer Inner>
? Inner extends Promise<any> ? MyAwaited<Inner> : Inner
: T extends Thenable<infer U> ? U : false

問題が少し変わって難しくなったみたいだった。実際難しかったけどなんとなくコツが掴めてきたかも?

PromiseLike

NozomuNozomu
NozomuNozomu

メモ

課題はCが truthy である場合の戻り値の型T、Cが falsy である場合の戻り値の型Fを受け取るIfを実装すること

type If<C, T, F> = any

とりあえずCをbooleanに制限する

- type If<C, T, F> = any
+ type If<C extends boolean, T, F> = any

あとはconditional typesを使って、trueかどうかで分岐させる

- type If<C extends boolean, T, F> = any
+ type If<C extends boolean, T, F> = C extends true ? T : F
NozomuNozomu

回答

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

メモ

課題はJavaScript のArray.concat関数を型システムに実装すること

とりあえずT,Uを配列に制限する

- type Concat<T, U> = any
+ type MyArray = unknown[] | readonly unknown[]
+ type Concat<T extends MyArray, U extends MyArray> = any

mapped types使えそうってことで途中経過

type MyArray = unknown[] | readonly unknown[]
- type Concat<T extends MyArray, U extends MyArray> = any
+ type Concat<T extends MyArray, U extends MyArray> = { [K in keyof T]: T[K] } 

あー閃いた、スプレッド構文でいけます

type MyArray = unknown[] | readonly unknown[]
- type Concat<T extends MyArray, U extends MyArray> = { [K in keyof T]: T[K] } 
+ type Concat<T extends MyArray, U extends MyArray> = [...T, ...U]
NozomuNozomu

回答

type MyArray = unknown[] | readonly unknown[]
type Concat<T extends MyArray, U extends MyArray> = [...T, ...U]
NozomuNozomu

おまけ

unknown[]型はreadonly unknown[]型に割り当て可能だからなくても良さそうだった

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

課題はJavaScriptのArray.include関数を型システムに実装すること

T[number]とconditional types組み合わせればいけそう

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

これだとIncludes<[1 | 2], 1>みたいなのがうまくいかなかった。それぞれの型で厳密に比較する必要がありそう

厳密比較するためにmapped typesと組み合わせてみたけどうまくいかず、、、

- type Includes<T extends readonly any[], U> = U extends T[number] ? true : false
+ type StrictExtend<T, U> = [T] extends [U] ? [U] extends [T] ? U : never : never;
+ type Includes<T extends readonly any[], U> = U extends { [K in keyof T]: StrictExtend<T[K], U> }[number] ? true : false

思いつかなかったため、回答を確認。まだまだだ、、、
再帰は盲点だった!inferと再帰的なのを使って先頭から順番に取ってきてる。

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

回答

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

メモ

課題は組み込みの型ユーティリティParameters<T>を使用せず、Tからタプル型を構築する型を実装すること

...args: any[]any[]の部分をinfer使って取り出せばいけそう

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

anyの部分、unknownにしても良かったかも
これにて初級終了!結構難易度に差があった

NozomuNozomu

回答

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

メモ

課題は組み込みの型ユーティリティReturnType<T>を使用せず、Tの戻り値の型を取得する型を実装すること

戻り値の部分をinferで取り出してあげれば良さそう

- type MyReturnType<T> = any
+ type MyReturnType<T extends (...args: unknown[]) => unknown> = T extends (...args: unknown[]) => infer R ? R : never

ただこれだと戻り値がユニオン型だとうまくいかないみたい

- type MyReturnType<T extends (...args: unknown[]) => unknown> = T extends (...args: unknown[]) => infer R ? R : never
+ type MyReturnType<T extends (...args: any[]) => unknown> = T extends (...args: any[]) => infer R ? R : never 

anyにしたらいけた
戻り値がユニオン型だとうまくいかないんじゃなくて、unknownをbooleanに入れようとして怒られていた

unknown使わずにいけないものかと考えたが、boolean使えば良さそうってなった

- type MyReturnType<T extends (...args: unknown[]) => unknown> = T extends (...args: unknown[]) => infer R ? R : never
+ type MyReturnType<T extends (...args: boolean[]) => unknown> = T extends (...args: boolean[]) => infer R ? R : never 

初の中級、ちょっと苦戦した

NozomuNozomu

回答

 type MyReturnType<T extends (...args: boolean[]) => unknown> = T extends (...args: boolean[]) => infer R ? R : never 
NozomuNozomu
NozomuNozomu

メモ

課題は組み込みの型ユーティリティOmit<T, K>を使用せず、TのプロパティからKを削除する型を実装すること

とりあえず、KはTのkeyに制限して、mapped typesを使ってみる

- type MyOmit<T, K> = any
+ type MyOmit<T, K extends keyof T> = { [key in keyof T]: T[key] }

あとは[key in keyof T]keyof TからKを省けばよさそうなので、Excludeを実装する

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

回答

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

おまけ

asを使うことで、keyの方でもconditional typesが使えて、keyをneverにすると消せる感じなのか

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

メモ

課題はKが指定されている場合に、Tの中のKのプロパティのみを読み取り専用にし、Kが指定されていない場合は、通常のReadonly<T>と同様に、すべてのプロパティを読み取り専用にするMyReadonly2<T, K>を実装すること

まずKの値を制限し、Kのデフォルトの型を設定することで省略して書けるようにする
省略した場合はkeyof Tとなるので全部readonlyにする。Kで指定したもの以外はまだ取り出せていない

- type MyReadonly2<T, K> = any
+ type MyReadonly2<T, K extends keyof T = keyof T> = { readonly [key in K]: T[key] }

あとはKで指定されていない値をOmitで取り出して、それを合成すればいい

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

中級は初級で作ったやつの組み合わせって感じがする

ジェネリクスのデフォルト型引数

https://typescriptbook.jp/reference/generics/default-type-parameter#型引数の制約と併用する

intersection型

https://typescriptbook.jp/reference/values-types-variables/intersection

NozomuNozomu

回答

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

おまけ

Exclude使ったパターンもあった

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

1行で書くならOmitをバラして、こんな感じ

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

中級「Deep Readonly」

課題はオブジェクトのすべてのパラメーター(およびそのサブオブジェクトを再帰的に)を読み取り専用にするDeepReadonly<T>を実装すること

NozomuNozomu

メモ

再帰的に読み取り専用にするのでconditional typesと組み合わせて再帰を使う
オブジェクトなのでmapped typesを使う
途中経過がいったんこんな感じ↓

- type DeepReadonly<T> = any
+ type DeepReadonly<T> = T extends Object
+   ? { readonly [key in keyof T]: T[key] extends Object | any[] ? DeepReadonly<T[key]> : T[key] }
+   : never

{ a: ()=> any }みたいなのが{ a: {} }になっちゃってる
()=>anyはObject型を継承してるみたいなので、条件追加

- type DeepReadonly<T> = T extends Object
-   ? { readonly [key in keyof T]: T[key] extends Object | any[] ? DeepReadonly<T[key]> : T[key] }
-   : never
+ type DeepReadonly<T> = T extends Object
+   ? { readonly [key in keyof T]: T[key] extends () => any ? T[key] : T[key] extends Object | any[] ?  DeepReadonly<T[key]> : T[key] }
+   : never
NozomuNozomu

回答

type DeepReadonly<T> = T extends Object
   ? { readonly [key in keyof T]: T[key] extends () => any ? T[key] : T[key] extends Object | any[] ?  DeepReadonly<T[key]> : T[key] }
   : never
NozomuNozomu

おまけ

keyof T[K]でObject、Arrayでないかの分岐をしているのか
こっちの方が簡潔でいいな

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

メモ

課題はタプルの値からユニオン型を生成するTupleToUnion<T>を実装すること
配列に制限して、T[number]で終わり

- type TupleToUnion<T> = any
+ type TupleToUnion<T extends unknown[]> = T[number]

めっちゃすぐできたのでunknownも使わないのも考えた
inferでvalueの部分取り出すだけ

type TupleToUnion<T> = T extends { [key in keyof T]: infer R } ? R : never
NozomuNozomu
NozomuNozomu

メモ

課題はオブジェクトでもクラスでも何でもいいので、 option(key, value) と get() の 2 つの関数を提供する型を定義すること

  • option では、与えられたキーと値を使って現在の config の型を拡張でき、最終的な結果は get で取得する
  • keystring のみを受け付け、value は任意の型を受け付けると仮定してもOK
  • 同じ key が 2 回渡されることはない

いったんoptionの返り値に渡されたkeyとvalueで作ったオブジェクトの型を設定し、getでoptionの返り値の型を取得する方針でいこうと思う

- type Chainable = {
-   option(key: string, value: any): any
-   get(): any
- }
+ type MethodReturnType<T> = T extends Array<infer R> ? R : never
+ type Chainable = {
+   option<T extends string, K>(key: T, value: K): { [key in T]: K }
+   get(): MethodReturnType<Chainable['option']>
+ }

ただこれではoptionを連鎖させても、optionの返り値が保持されていない
この後色々試したがかなり時間がかかってしまったため、回答を確認した

ジェネリクスのデフォルト型引数に気づけなかった、、、
これが分かれば後はChainableを再起的な感じで使ってTに値を保持するだけ

type Chainable<T = {}> = {
  option<K extends string, V>
     (key: K extends keyof T ? never : K, value: V) : Chainable<T & {[key in K]: V}>
  get(): T
}
NozomuNozomu

回答

type Chainable<T = {}> = {
  option<K extends string, V>
     (key: K extends keyof T ? never : K, value: V) : Chainable<T & {[key in K]: V}>
  get(): T
}
NozomuNozomu
NozomuNozomu

課題は配列 T を受け取り、その最後の要素の型を返す汎用的な Last<T> を実装すること

スプレッド構文とinferを併用するだけ

- type Last<T extends any[]> = any
+ type Last<T extends unknown[]> = T extends [...infer rest, infer R] ? R : never
NozomuNozomu

おまけ

[T["length"]]で元々の配列の要素数+1番目を取得している
配列を1つ先頭に増やすことでそれを可能にしている、面白い

type Last<T extends any[]> = [any, ...T][T["length"]];
NozomuNozomu
NozomuNozomu

メモ

課題は配列 T を受け取り、最後の要素を除いた配列を返す汎用的な Pop<T> を実装すること

これもスプレッド構文とinferを併用するだけ
また、空配列が渡されたら空配列を返すようにする

- type Pop<T extends any[]> = any
+ type Pop<T extends unknown[]> = T extends [...infer R, infer _] ? R : []

使わない部分のinferを_にするの見た目的に良さげなので使ってみた

NozomuNozomu

回答

 type Pop<T extends unknown[]> = T extends [...infer R, infer _] ? R : []
NozomuNozomu
NozomuNozomu

メモ

課題はPromise ライクなオブジェクトの配列を受け取る関数 PromiseAll に型を付けること

  • 戻り値は Promise<T> である必要がある

まず、渡された引数の型をPromiseにしてそのまま返す感じで実装してみる
1つ目は通ると思ったけど、readonlyがついててうまくいかないみたい

- declare function PromiseAll(values: any): any
+ declare function PromiseAll<T>(values: T): Promise<T>

readonlyがついている場合は-readonlyにして修飾子の削除を行うようにする

- declare function PromiseAll<T>(values: T): Promise<T>
+ type RemoveReadOnly<T> = { -readonly [key in keyof T]: T[key] }
+ declare function PromiseAll<T>(values: T): Promise<RemoveReadOnly<T>>

配列にPromise<number>みたいなのがある場合はnumberの部分を取り出したいので、inferを使う

type RemoveReadOnly<T> = { -readonly [key in keyof T]: T[key] }
+ type ReturnPromise<T> = { [key in keyof T]: T[key] extends Promise<infer R> ? R : T[key] }
- declare function PromiseAll<T>(values: T): Promise<RemoveReadOnly<T>>
+ declare function PromiseAll<T>(values: T): Promise<RemoveReadOnly<ReturnPromise<T>>>

うーん、一個ずつ取り出すやり方が思い付かなかったので回答を見てみる
スプレッド構文で配列の型を1つずつ取得できるみたい

type MyAwaited<T> = T extends Promise<infer R> ? MyAwaited<R> : T;
declare function PromiseAll<T extends unknown[]>(values: readonly [...T]):
  Promise<{ [key in keyof T]: MyAwaited<T[key]> }>;

修飾子の削除

https://typescriptbook.jp/symbols-and-keywords#--修飾子の削除-ts

NozomuNozomu

回答

type MyAwaited<T> = T extends Promise<infer R> ? MyAwaited<R> : T;
declare function PromiseAll<T extends unknown[]>(values: readonly [...T]):
  Promise<{ [key in keyof T]: MyAwaited<T[key]> }>;
NozomuNozomu
NozomuNozomu

メモ

課題はCat | Dog という Union 型に共通する type というフィールドを使って、対応する型を取得すること

typeの部分をinferで取り出して、その取り出した型がTと同じの場合にUを返すようにすれば良さそう
conditional typesがジェネリクスに作用する際に、ユニオン型の場合は展開される(Distributive Conditional Types)を利用する

- type LookUp<U, T> = any
+ type LookUp<U, T> = U extends { type: infer R } ? R extends T ? U : never : never
NozomuNozomu

回答

 type LookUp<U, T> = U extends { type: infer R } ? R extends T ? U : never : never
NozomuNozomu

おまけ

めっちゃシンプルな回答があった
infer使って取り出さなくても、Tの値で絞り込めたか

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

TはUnion型ではないけどmapped types使ってやってる
上のやつをちょっと回りくどくした感あるな

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

メモ

課題は文字列を受け取り、先頭の空白を削除した新しい文字列を返す TrimLeft<T> を実装すること

${infer A}${infer B} のテンプレートリテラル型を使うことで、Aに先頭の文字列が入り、Bが残りの文字列となる
これを利用して' ''\n''\t'の場合は削除するようにする

- type TrimLeft<S extends string> = any
+ type TrimLeft<S extends string> = S extends `${infer A}${infer B}` ? A extends ' '|'\n'|'\t' ? TrimLeft<B> : S : S
NozomuNozomu

回答

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

おまけ

Spaceのものかどうか分かればいいので、先頭の文字列をinferで取り出す必要はないってことか
賢い

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

メモ

課題は文字列を受け取り、両端の空白を削除した新しい文字列を返す Trim<T> を実装すること

TrimLeftみたいにSpaceを定義して、${Space}${infer R}みたいな感じにして空白を取り除けばいい
今回は両端なので逆パターンも

- type Trim<S extends string> = any
+ type Trim<S extends string> = S extends `${Space}${infer R}` | `${infer R}${Space}` ? Trim<R> : S

Sが${Space}${infer R}${infer R}${Space}のどちらかに互換があればいいのでinferで型名は同じでいい

NozomuNozomu

回答

 type Trim<S extends string> = S extends `${Space}${infer R}` | `${infer R}${Space}` ? Trim<R> : S
NozomuNozomu
NozomuNozomu

メモ

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

まず先頭の文字を取り出していじれる状態にする

- type MyCapitalize<S extends string> = any
+ type MyCapitalize<S extends string> = S extends `${infer A}${infer B}` ? `${A}${B}` : S

スマートなやり方が思いつかなかったので力技でやる

+ type ToUpperCase<T> =
+   T extends 'a' ? 'A'
+   : T extends 'b' ? 'B'
+   : T extends 'c' ? 'C'
+   : T extends 'd' ? 'D'
+   : T extends 'e' ? 'E'
+   : T extends 'f' ? 'F'
+   : T extends 'g' ? 'G'
+   : T extends 'h' ? 'H'
+   : T extends 'i' ? 'I'
+   : T extends 'j' ? 'J'
+   : T extends 'k' ? 'K'
+   : T extends 'l' ? 'L'
+   : T extends 'm' ? 'M'
+   : T extends 'n' ? 'N'
+   : T extends 'o' ? 'O'
+   : T extends 'p' ? 'P'
+   : T extends 'q' ? 'Q'
+   : T extends 'r' ? 'R'
+   : T extends 's' ? 'S'
+   : T extends 't' ? 'T'
+   : T extends 'u' ? 'U'
+   : T extends 'v' ? 'V'
+   : T extends 'w' ? 'W'
+   : T extends 'x' ? 'X'
+   : T extends 'y' ? 'Y'
+   : T extends 'z' ? 'Z'
+   : T
- type MyCapitalize<S extends string> = S extends `${infer A}${infer B}` ? `${A}${B}` : S
+ type MyCapitalize<S extends string> = S extends `${infer A}${infer B}` ? `${ToUpperCase<A>}${B}` : S

絶対もっと短くできそう、、、

NozomuNozomu

回答

 type ToUpperCase<T> =
   T extends 'a' ? 'A'
   : T extends 'b' ? 'B'
   : T extends 'c' ? 'C'
   : T extends 'd' ? 'D'
   : T extends 'e' ? 'E'
   : T extends 'f' ? 'F'
   : T extends 'g' ? 'G'
   : T extends 'h' ? 'H'
   : T extends 'i' ? 'I'
   : T extends 'j' ? 'J'
   : T extends 'k' ? 'K'
   : T extends 'l' ? 'L'
   : T extends 'm' ? 'M'
   : T extends 'n' ? 'N'
   : T extends 'o' ? 'O'
   : T extends 'p' ? 'P'
   : T extends 'q' ? 'Q'
   : T extends 'r' ? 'R'
   : T extends 's' ? 'S'
   : T extends 't' ? 'T'
   : T extends 'u' ? 'U'
   : T extends 'v' ? 'V'
   : T extends 'w' ? 'W'
   : T extends 'x' ? 'X'
   : T extends 'y' ? 'Y'
   : T extends 'z' ? 'Z'
   : T
 type MyCapitalize<S extends string> = S extends `${infer A}${infer B}` ? `${A}${B}` : S
 type MyCapitalize<S extends string> = S extends `${infer A}${infer B}` ? `${ToUpperCase<A>}${B}` : S
NozomuNozomu

おまけ

extends羅列よりわかりやすい

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
NozomuNozomu
NozomuNozomu

メモ

課題は文字列Sに含まれる文字FromToに一度だけ置き換える型Replace<S, From, To>を実装すること

お馴染みのinferでFrom以外の部分の文字を取得して、Fromの部分だけToに変える

- type Replace<S extends string, From extends string, To extends string> = any
+ type Replace<S extends string, From extends string, To extends string> = S extends `${infer Prefix}${From}${infer Suffix}` ? `${Prefix}${To}${Suffix}` : S

Replace<'foobarbar', '', 'foo'>だけうまくいかない
空文字列無視するようにする

- type Replace<S extends string, From extends string, To extends string> = S extends `${infer Prefix}${From}${infer Suffix}` ? `${Prefix}${To}${Suffix}` : S
+ type Replace<S extends string, From extends string, To extends string> =
+  From extends ''
+  ? S
+  : S extends `${infer Prefix}${From}${infer Suffix}`
+  ? `${Prefix}${To}${Suffix}`
+  : S
NozomuNozomu

回答

type Replace<S extends string, From extends string, To extends string> =
  From extends ''
  ? S
  : S extends `${infer Prefix}${From}${infer Suffix}`
  ? `${Prefix}${To}${Suffix}`
  : S
NozomuNozomu
NozomuNozomu

メモ

課題は文字列Sに含まれる部分文字列FromToに置き換える型ReplaceAll<S, From, To>を実装すること

さっきのReplaceを再帰的に使ってみる

- type ReplaceAll<S extends string, From extends string, To extends string> = any
+ type ReplaceAll<S extends string, From extends string, To extends string> =
+   From extends ''
+   ? S
+   : S extends `${infer Prefix}${From}${infer Suffix}`
+   ? ReplaceAll<`${Prefix}${To}${Suffix}`, From, To>
+   : S

置き換えた箇所でまた置き換え対象の文字列ができる場合に対応できていなかった
先頭から置き換えてそうなので、Toの後(Suffix)を渡すようにすればいける

type ReplaceAll<S extends string, From extends string, To extends string> =
  From extends ''
  ? S
  : S extends `${infer Prefix}${From}${infer Suffix}`
-   ? ReplaceAll<`${Prefix}${To}${Suffix}`, From, To>
+   ? `${Prefix}${To}${ReplaceAll<`${Suffix}`, From, To>}`
  : S
NozomuNozomu

回答

type ReplaceAll<S extends string, From extends string, To extends string> =
 From extends ''
 ? S
 : S extends `${infer Prefix}${From}${infer Suffix}`
   ? `${Prefix}${To}${ReplaceAll<`${Suffix}`, From, To>}`
 : S
NozomuNozomu
NozomuNozomu

メモ

課題は与えられた関数型 Fn と任意の型 A に対して、第一引数に Fn を取り、第二引数に A を取り、Fn の引数に A を追加した関数型 G を生成すること

まずFnをFuctionに制限して、inferとスプレッド構文を組み合わせて引数の型を取得する

- type AppendArgument<Fn, A> = any
+ type AppendArgument<Fn extends Function, A> = Fn extends (...args: infer R) => infer S ? (x: A, ...args: R) => S : never;

ただこれだと引数xが先に来てしまっているので後ろに追加したい
...argsで展開する中にxも入れられればいいんじゃね、と思って思いつきで書いた
ラベル付きタプルっていう機能らしい?

- type AppendArgument<Fn extends Function, A> = Fn extends (...args: infer R) => infer S ? (x: A, ...args: R) => S : never;
+ type AppendArgument<Fn extends Function, A> = Fn extends (...args: infer R) => infer S ? (...args: [...args: R, x: A]) => S : never;

ラベル付きタプル

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#labeled-tuple-elements

NozomuNozomu

回答

 type AppendArgument<Fn extends Function, A> = Fn extends (...args: infer R) => infer S ? (...args: [...args: R, x: A]) => S : never;
NozomuNozomu

おまけ

別にラベル付きでxにする必要なかったみたい

type AppendArgument<Fn extends Function, A> = Fn extends (...args: infer R)=> infer S ? (...args: [...R, A])=> S : never
NozomuNozomu
NozomuNozomu

メモ

課題はUnion 型を Union 型の値の順列を含む配列に変換する順列型を実装すること

ユニオン型を配列にするのが思いつかなかった、、、
回答見てみる

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

あー、リテラル型でconditional typesを使った時に展開されるの忘れてた
[T] extends [never]のように配列にしているのはnever以外で意図しない値になってしまうからみたい

type P<T> = T extends never ? true : false;
type A1 = P<never> //never 
type A2 = P<any> //boolean 

type Q<T> = [T] extends [never] ? true : false;
type B1 = Q<never> //true
type B2 = Q<any> //false

[T] extends [never]もそうだし、UにTを入れておいたりと難易度高くなってきた

NozomuNozomu

回答

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

メモ

課題はString#length と同じように、文字列リテラルの長さを計算する型を作ること

inferで1文字ずつ配列に入れてT['length']使う感じでいけそう
値の保持は一個前の問題で使ったデフォルト値の代入を使う

- type LengthOfString<S extends string> = any
+ type LengthOfString<S extends string, T extends unknown[] = []> = S extends `${infer A}${infer B}` ? LengthOfString<B, [A, ...T]> : T['length']

だいぶスラスラできた

NozomuNozomu

回答

type LengthOfString<S extends string, T extends unknown[] = []> = S extends `${infer A}${infer B}` ? LengthOfString<B, [A, ...T]> : T['length']
NozomuNozomu

おまけ

Tはstringの配列でよかった

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

メモ

課題は受け取った配列をフラット化した型を出力する型を作ること

T[number]と再帰を使って、ユニオン型でフラット化するところまではいけた

- type FlattenUnion<T> = any
+ type FlattenUnion<T> = T extends unknown[] ? FlattenUnion<T[number]> : T
+ type Flatten<T extends unknown[]> = FlattenUnion<T[number]>

ただ順番が変わってしまうし、ユニオン型を配列に変換する方法が思いつかない、、、

inferで配列の1つ目を取得する方法にしたらスムーズにいけた

- type FlattenUnion<T> = T extends unknown[] ? FlattenUnion<T[number]> : T
- type Flatten<T extends unknown[]> = FlattenUnion<T[number]>
+ type Flatten<T extends unknown[], U extends unknown[] = []> = T extends [infer R, ...infer S]
+   ? R extends unknown[]
+     ? Flatten<[...R, ...S], U>
+     : Flatten<[...S], [...U, R]>
+   : U
NozomuNozomu

回答

 type Flatten<T extends unknown[], U extends unknown[] = []> = T extends [infer R, ...infer S]
   ? R extends unknown[]
     ? Flatten<[...R, ...S], U>
     : Flatten<[...S], [...U, R]>
   : U
NozomuNozomu

おまけ

以下のような回答もあって、よりシンプルな感じがあって良さげだが@ts-expect-errorを満たしていなかった

type Flatten<T> = T extends []
  ? [] 
  : T extends [infer First, ...infer Rest]
    ? [...Flatten<First>, ...Flatten<Rest>]
    : [T]
NozomuNozomu
NozomuNozomu

メモ

課題はインターフェースに新しいフィールドを追加する型を実装すること

普通のmapped typesにUとVを添えるだけ

- type AppendToObject<T, U, V> = any
+ type AppendToObject<T, U extends string, V> = { [key in keyof T | U]: key extends keyof T ? T[key] : V }
NozomuNozomu

回答

type AppendToObject<T, U extends string, V> = { [key in keyof T | U]: key extends keyof T ? T[key] : V }
NozomuNozomu
NozomuNozomu

メモ

課題はsrting, number または bigint を受け取り、正の数を出力する Absolute 型を実装すること

numberとbigintの場合はstringに変換して、stringでマイナスがある場合はinferで取り除いて、それ以外はそのまま

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

回答

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

おまけ

numberとbigintの場合はstringに変換する部分が省略できていてすごい

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

メモ

課題は受け取った文字列を Union 型に変換する型を実装すること

inferで文字列を1文字ずつ取得して、再帰を使ってユニオン型にすればOK

- type StringToUnion<T extends string> = any
+ type StringToUnion<T extends string> = T extends `${infer R}${infer S}` ? R | StringToUnion<S> : never
NozomuNozomu

回答

type StringToUnion<T extends string> = T extends `${infer R}${infer S}` ? R | StringToUnion<S> : never
NozomuNozomu

中級「Merge」

課題は2つの型をマージして新しい型を作ること

mapped typesを使って、keyが被っている場合は2つ目の方を採用するので今回で言うとSを優先するようにして完成

- type Merge<F, S> = any
+ type Merge<F, S> = { [key in keyof F | keyof S]: key extends keyof S ? S[key] : key extends keyof F ? F[key] : never }
NozomuNozomu

回答

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

メモ

課題はキャメルケースもしくはパスカルケースの文字列を、ケバブケースに置換する型を実装すること

まず大文字と小文字の対応する型のリストを作って、頭文字だけリストでそのまま置き換えるようにする

+ type Atoa = {
+   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 KebabCase<S> = any
+ type KebabCase<S> = S extends `${infer Initial}${infer R}`
  ? `${Initial extends keyof Atoa ? Atoa[Initial] : Initial}${R}`
  : ''

先頭の文字以外は変換するときに-が入るので、1文字ずつ置き換える用の型を作って完成

type Atoa = {
  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 KebabCaseCommon<S, T extends string = ''> = S extends `${infer R}${infer S}`
+   ? R extends keyof Atoa
+   ? KebabCaseCommon<S, `${T}-${Atoa[R]}`>
+     : KebabCaseCommon<S, `${T}${R}`>
+     : T
type KebabCase<S> = S extends `${infer Initial}${infer R}`
-   ? `${Initial extends keyof Atoa ? Atoa[Initial] : Initial}${R}`
+   ? `${Initial extends keyof Atoa ? Atoa[Initial] : Initial}${KebabCaseCommon<R>}`
  : ''
NozomuNozomu

回答

type Atoa = {
 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 KebabCaseCommon<S, T extends string = ''> = S extends `${infer R}${infer S}`
  ? R extends keyof Atoa
  ? KebabCaseCommon<S, `${T}-${Atoa[R]}`>
    : KebabCaseCommon<S, `${T}${R}`>
    : T
type KebabCase<S> = S extends `${infer Initial}${infer R}`
   ? `${Initial extends keyof Atoa ? Atoa[Initial] : Initial}${KebabCaseCommon<R>}`
   : ''
NozomuNozomu

おまけ

ユーティリティ型のUncapitalizeを使って、先頭の文字が変化したら(S2 extends Uncapitalize<S2>がfalseだったら)大文字の前に-を入れている
基本全ての文字をUncapitalizeに通しているのもあるが、シンプルにまとまっていてすごい

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

Uncapitalize

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

NozomuNozomu
NozomuNozomu

メモ

課題は2つのObjectの差のObjectの型を出す型を作ること

2つのkeyの差分を出して、mapped typesでそれを表示するだけ

- type Diff<O, O1> = any
+ type DiffKey<A, B> = A extends B ? never : A
+ type Diff<O, O1> = {
+   [key in DiffKey<keyof O, keyof O1> | DiffKey<keyof O1, keyof O>]: key extends keyof O ? O[key]: key extends keyof O1 ? O1[key]: never
+ }

valueの方の比較も必要になったらちょっと大変そう

NozomuNozomu

回答

 type DiffKey<A, B> = A extends B ? never : A
 type Diff<O, O1> = {
   [key in DiffKey<keyof O, keyof O1> | DiffKey<keyof O1, keyof O>]: key extends keyof O ? O[key]: key extends keyof O1 ? O1[key]: never
 }
NozomuNozomu

おまけ

めちゃくちゃシンプル
keyof (O | O1)がOまたはO1のkeyの型なので、OとO1の両方とも持っているkeyが取れるのか
(=keyof O & keyof O1
オブジェクトの場合は&で増えるけど、それ以外は基本絞り込まれるんだっけか

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

Exclude使ってるやつ
keyof (O & O1)とか(O & O1)[K]の書き方いいな

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

ユーティリティ型使うのが増えてきた