問題 & 解答
二つの引数T
、K
をとり、K
が指定されていればT
のそのプロパティを、指定されていなければすべてのT
のプロパティを読み取り専用に変換するMyReadonly<T, K>
を実装する。
type MyReadonly2<T, K extends keyof T = keyof T> = { readonly [P in K]: T[P] } & Omit<T, K>
「readonly
なK
で指定されているプロパティ & T
に含まれるK
以外のプロパティ」を目指して作っていきます。
readonly
なK
で指定されているプロパティを作る
1. これが通常のReadonlyの実装です。
type MyReadonly2<T, K extends keyof T> = { readonly [P in T]: T[P] }
このままだとK
の値がreadonly
にならないので、mapped typesでぐるぐるするところを変えます。
type MyReadonly2<T, K extends keyof T> = { readonly [P in T]: T[P] readonly [P in K]: T[P]}
これによりK
に渡されたプロパティはreadonly
になります。
T
に含まれるK
以外のプロパティ
2. これだけだとK
に含まれるプロパティしか含まれていません。そのため、K
に渡されなかったプロパティの型を取得する必要があります。これはT
の中からK
に該当するプロパティを除いたものです。
例えば以下のようなT
とK
を渡すことを考えます。
// こっちがTtype Music ={ name: string, artist: string, releaseYear: number}
// こっちがKtype ReadonlyRequiredParams = "artist" | "releaseYear"
最終的にMyReadonly2
に期待するのは次のような形式なので、name: string
を取り出せれば良いはずです。
type Expected = { name: string, // TODO: これから取得したい readonly artist: string, // { readonly [P in K]: T[P] } で表現される readonly releaseYear: number // { readonly [P in K]: T[P] } で表現される}
これは昨日出てきたOmit
(組み込みの型の方です)を使用して、Omit<T, K>
の形式で取り出すことができます。
type Music2 = Omit<Music, ReadonlyRequiredParams>// type Music2 = { name: string; }
3. 1と2を合体
これらをインターセクション型で繋ぎこむと、解答を得ることができます。
type MyReadonly2<T, K extends keyof T> = { readonly [P in K]: T[P] } & Omit<T, K>
K
のデフォルト値を設定する
4. …と思ったらまだエラーが出ています。この型はK
を省略可能なのでそこでひっかります。K
は参照されるので何かしらの値を入れておく必要があります。
TypeScriptは型引数にデフォルト値を取ることができます。
https://typescriptbook.jp/reference/generics/default-type-parameter
今回は「K
を指定しなかった場合、すべてのプロパティがreadonly
になる」ので、K
には「T
のプロパティすべて」を設定します。
type MyReadonly2<T, K extends keyof T K extends keyof T = keyof T>
他の人の解答を見ていたら、Omit<T, K>
の部分を& T
で繋ぎ込んでいる解答もあったのですが、これではだめでした。
interface Todo1 { title: string description?: string completed: boolean}
interface Todo2 { readonly title: string description?: string completed: boolean}
type TodoX = Todo1 & Todo2
const todoX: TodoX = { title: "JavaScriptを勉強する", completed: true}
todoX.title = "TypeScript" // titleはreadonlyではない。
感想
う〜ん、最後のインターセクション型の挙動についてはドキュメントをざっと読んだのですが、期待する記述は見つけられませんでした。readonly
だけならいいんですが、他にも自分が理解できていない部分があると怖いです。