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。
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したもの)プロパティを持っているなら、その型を返すなければ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
楽勝過ぎた。
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どうするか
他の解答を見て参考にしてみる。
なるほど、再度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で取得するまではできた。
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