type-challenges 学習録
進め方
参考
手順
-
READMEから挑戦した問題を選ぶ
バッジをクリックすると問題詳細ファイルに移動 -
「TS 挑戦する」ボタンを押すと、TypeScript Playgroundで問題にチャレンジすることができる
-
回答を共有 or 解答を確認
Pickをやってみる
Pick<T, Keys>のユーティリティ関数の挙動自体を知らなかった。。
TS弱者を自覚。。(今まで雰囲気でTSを書いていた。)
Tの型からKeysで指定したキーだけを含むオブジェクト型を返すユーティリティ型とのこと。
type MyPick<T, K> = any
easyはTSの基本文法を知る為の問題らしい。
文法的知識がないから取り掛かりがわからん。。
PickよりもIfが良いみたい。
先にそっちを解こう。
If
Ifで必要な知識は以下とのこと
型引数の制約
要約:ジェネリクスので受け取る型を制限する方法 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;
Pick
もう一度 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と型制約をつかえばいいはずだが、わからん。
を参考にすると、以下の記事が紹介されているので読む。
なるほど自力で解けた。
type MyPick<T, K extends keyof T> = {
[key in K]: T[key]
}
考え方は、
- ジェネクスKに型制約をつける
- Kはunion型だから、それをMapped Typesで書ける
- keyにはunion型をバラした値が入ってくるからそれを利用してT[key]でTで指定されている型が取得できる
Readonly
また詰まった。
ユーティリティ型のReadOnlyはObject.freeze的な動きをする。
のは分かる。
それをTSでどう表現するか
プロパティに対してreadonly修飾子をつけることで、readonlyにできるよう。
let obj: {
readonly foo: number;
};
これとMapped Typesを合わせればよかった。
Tuple to Object
Mapped Typesでkeyof, typeofを使う方向だとは思う。
typeofはconstで定義された実体の値を型化するものなので、この場合は違う気がしてきた。
array型は、T[number]と記述することで値を型として抽出できる。裏挙動すぎる。
あとはMapped Typesを使う。
解説があった
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。
type TupleToObject<T extends readonly any[]> = {
[P in T[number]] : P
}
First of Array
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型を理解する
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
せっかくだから調べてみよう。
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
型定義のスプレッドに慣れていないので一応見る。
↓このユニオン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
Length of Tuple
as constがついていたら readonly
をつけることを忘れない。
楽勝過ぎた。
type Length<T extends readonly any[]> = T['length']
が、
TS的に書くならneverも考慮した方が良い気がしてきた。
type-challenges的にはテストコードを満たす挙動ができていたらok
実務的にはneverを考慮するくらいでいいか
Exclude
例
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という概念をしらないといかんらしい。
ここで言う「分配」はユニオン型の事を指している
ユニオン型の場合の 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
Awaited
Promiseライクな型が内包する型の取得
これも文法の話。
type X = Promise<string>
MyAwaited<X> // string
type MyAwaited<T> = Promise<U>
上記を観察
Promise型にジェネリクスでstringが渡されている
Primiseに渡されている
型が型を内包しているように見える
Promise オブジェクトの型はジェネリクスを使って Promise<Type> というような形式
Promiseという関数がstringを返す形になってる
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 ◯◯の部分は関数のように再帰的に指定ができる学び
Concat
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を学習する必要があるとのこと
array型も普通の配列と同じ感覚でスプレッド演算子が使えるんだった。
type Concat<T extends readonly any[], U extends readonly any[]> = [...T, ...U]
Includes
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もやったことある気がする
再帰か、これ正式にリカーシブコンディショナルタイプっていうのか
Variadic Tuple Typesはもうちょい深める
タプル型の中に...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が等価かどうかの判定らしい
Push
瞬殺
type Push<T extends unknown[], U> = [...T, U]
タプルの展開はVariadic Tuple Types
可変長タプル
Unshift
瞬殺。Pushとやってることが同じ。
type Unshift<T extends unknown[], U> = [U, ...T]
Parameters
ついに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
になるのを理解
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>インターフェイスがビルトインで存在するとのこと
↓これでいけた
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 : []
Get Return Type
infer で簡単にできる。
Parametersと考え方が同じ。
ジェネリクス制約を書く場合に、引数の型の書き方に気をつける。
type MyReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer U ? U : never;
Omit
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という知識が必要らしい
Mapped Typesを利用して元のプロパティから新しいプロパティを生成したり、あるプロパティを除外する為にはas句を使用する
Capitalizeというビルトインの型がTS4.1からあるらしい
そして、その型定義でinstrinsicというワードが出てきた。
type Capitalize<S extends string> = 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] }
Readonly 2
困りごと
2 argument typeのときの絞り方
readonlyの場合分けの書き方
ヒントを見た
インターセクション型を使うらしい(&で繋ぐやつ)
イメージ
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]
}
Deep Readonly
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
というビルトイン型を利用する
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をカマスと自然と配列の展開が行われる
Tuple to Union
パット見簡単に見える。
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
Chainable Options
チェイン可能なオプション
また取っ掛かりのわからないやつきた。
type Chainable = {
option(key: string, value: any): any
get(): any
}
他の方々の解答を早速覗いてみる。
記述のポイントは、以下だと感じた。
- ジェネリクス制約を書く場所を意識
- 型全体のジェネリクス+初期値
- メソッドに対するジェネリクス
- 再帰
この辺りからeasyの時よりも各々の書き方の個性が出てきているように思う。
解決方法が思いつかないときは最小限の解から考えてみる。
この人の思考の流れがいい。
まずは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(ユーティリティ型)を利用している人がいる
Record<Keys, Types>
キーと値の型を指定することで以下のような型が作れる。
type StringNumber = Record<string, number>;
const value: StringNumber = { a: 1, b: 2, c: 3 };
想像以上にむずい。
いくつかissueを見てみる。
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
最終解
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]
}
type-challengesのmiddleからユーティリティ型を理解しておいたほうがスマートに問題に取り組めるように思えてきた。
Last of Array
素直にスプレッド構文とinferを使って、Conditional Typeでいけた
type Last<T extends any[]> = T extends [...any, infer L] ? L : never;
Pop
この課題も 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
Promise.all
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を再帰的に回してラップを解くユーティリティ関数とのこと
今回の課題は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上の配列の扱いは、キーが数値のオブジェクト型としてとらえたら良さげ。
Type Lookup
簡単そうで取っ掛かりが見えない。
感覚的に書くと以下。これだと通らない。
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}
で表せばいいみたい。
Trim Left
Conditional Types + infer + 文字列リテラル と推測
直感的に書く。無理
type TrimLeft<S extends string> = S extends infer ' ' + T ? T : S
テンプレートリテラル型にも使える
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;
Trim
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>>;
Capitalize
大文字小文字の取得ってどうやればいいんだろう。
取っ掛かりがわからない。
upper的なユーティリティ型があるかどうか。
→あったUppercase、Lowercaseもあった
リテラル型 + inferで最初の文字とそれ以外という区別の仕方
単純に以下のように書いた場合、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;
内部実装気になる
Replace
今回のお題もリテラル型
できた
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;
ReplaceAll
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;
Append Argument
こんな感じで考えたけど、テストパターンの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
Permutation
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の挙動が頭の中でトレースできないからこれ系の課題はむずい。
Length of String
型でカウントアップができる?
インクリメントの機構がないんじゃ?
取り掛かりが分からない。。
解答を見る。
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, ...]]]の様な形で展開されてしまう。
なので、配列をフラットに保ちたいときはレスト構文を使う必要がある。
Flatten
こんな雰囲気のことを書くのかなと思ったら違った。
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]
Append to object
感覚的には、以下のように書きたい。
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;
};
Absolute
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}`
String to Union
今までの知識の応用だった為、一瞬で解けた。
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]
Merge
解けた。
これも以前やった問題の応用で、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>
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] }
KebabCase
以前に大文字に変換するユーティリティ関数があることをやった。
シンプルな問題かなと思いつつ、やりだすと大文字から小文字に変換する際の制約があることに気づく。
一応、大文字から小文字への変換処理は以下で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を使えばできると学習
Diff
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型を返すユーティリティ型です。
AnyOf
和訳がないのでメモ
- 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}
は学びになった。
IsNever
Neverの判定について、T extends never だと判定できない。。
この問題のレベルで、この難易度は流石に無いと思ったけど、その通りだった。
type IsNever<T> = T extends never ? true : false;
neverはNaNみたいな存在なのか。
neverが判定できない問題
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的な判定が出来ないので、上記の様になると理解しておこう。
IsUnion
ユニオンの判断、感覚的に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
ReplaceKeys
ジェネリクスの意味について
1つ目→精査対象のオブジェクトのユニオン
2つ目→検索対象のキーのユニオン
3つ目→対象のキーの変換指定
改めてDistributive周りの挙動を見る
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] }
他の解答も同じ様な感じだった。
もう一歩。
Remove Index Signature
こういうことがしたい。
当たり前のように違う。
type RemoveIndexSignature<T extends {[key: string]: any}> = {
[K in keyof T]: K extends [key: string | number | symbol] ? never : T[K]
}
Index Signatureを改めて復習
オブジェクトの中で [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
で表現されるユニオン型みたい。
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のどこかで引っかかるという理解。
Percentage Parser
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度書きそうになったら、ユニオンで一つにまとめられないかを考慮する。
Drop Char
ちょっとダサめだけど出来た。
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の形が成り立つからこの記述でいいのか。勉強になる。
MinusOne
いよいよ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でその対象を取得してるのか。
PickByType
これくらいは楽勝で出来る。
type PickByType<T extends Record<string, any>, U> = {
[K in keyof T as T[K] extends U ? K : never]: U
}
解答を見るが、特にトリッキーなものはなし。
一瞬で出来たし、成長してる。
StartsWith
type StartsWith<T extends string, U extends string> = T extends `${U}${infer Last}` ? true : false
これも出来た。
急に難易度が落ちてきた。
Ends With
type EndsWith<T extends string, U extends string> = T extends `${infer First}${U}` ? true : false
できた。
回答を見たら以下でも良かった。
type EndsWith<T extends string, U extends string> = T extends `${any}${U}` ? true : false
PartialByKeys
こんな感じで考えたけど違う。
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 → 与えられたオブジェクト型から指定したキーを除いたオブジェクト型を返すユーティリティ型
RequiredByKeys
前回課題と逆の様な実装をすれば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 の意味のまま使われている。
Mutable
readonlyを取り払う。
Mapped Typesをかければ良いわけじゃない。
readonlyかどうかの判定はどうするか。
解答を見る。
type Mutable<T> = {-readonly [K in keyof T]: T[K]}
Mapped Typesに -readonly
をつければよい。
-
って何!初めて知った。
ちゃんと仕様に書いてあった。
-?
でオプショナルも消せる(前の課題でやったやつ。)
OmitByType
これに似たような課題を前にやったような気がする。
解けた。
type OmitByType<T, U> = { [K in keyof T as T[K] extends U ? never : K]: T[K] }
絶対に課題の内容被ってると思う。
解答を見る。一緒だった。
ObjectEntries
こういうのは思いついた。
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]
この解答に、インデックス型の分配についても書かれていた。ただし、満たせていないテストケースもある。
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の仕様を正しく理解していないと解けない問題だった。
Shift
これに似たような課題を前にやった気がする。
できた。MappedTypesばかりを触っていると、MappedTypesで解決する思考に偏ってしまう。
type Shift<T extends any[]> = T extends [any, ... infer U] ? [...U] : []
解答を見る。大体同じ。
Tuple to Nested Object
本当にざっくり、最初の段階として以下を考えてみた。
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に使えるの初めて知った。
Reverse
できた。
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[]でいい。テストケースに影響されずに汎用的な型を考えることを忘れない。
Flip Arguments
関数の引数の順序を逆転させる問題とのこと
引数の指定で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。
FlattenDepth
配列型の展開について
一旦以下を書いてみた。いくつかのテストケースがクリアできていない。
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>] とするかで分岐
なるほど。
パターンに分けてちゃんと実装する方法を丁寧に考えないと。
課題が多段階で思考する必要がある場合に早々に諦めがち。
BEM style string
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]}`}`
InorderTraversal
val, left, rightをプロパティに持つObjectから配列型を作る。
配列型の値を格納していくジェネリクスが必要そう。
leftがnullならvalを配列に格納し、rightなら再帰的な精査をする感じかな。
普段 type + インターセクション型 での継承もどきしかしていないからinterface + extendsの正式な継承を忘れている。
書いてみた。雰囲気は以下のようなことがしたい。
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を取り除いた型を返す。
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']>]
: [];
配列展開系の場合、ジェネリクスなどを使わずに真っ先に直接格納 + レスト構文を使うようにする。