type-challenges 13日目: 8-Readonly 2

問題 & 解答

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

二つの引数TKをとり、Kが指定されていればTのそのプロパティを、指定されていなければすべてのTのプロパティを読み取り専用に変換するMyReadonly<T, K>を実装する。

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

readonlyKで指定されているプロパティ & Tに含まれるK以外のプロパティ」を目指して作っていきます。

1. readonlyKで指定されているプロパティを作る

これが通常の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になります。

2. Tに含まれるK以外のプロパティ

これだけだとKに含まれるプロパティしか含まれていません。そのため、Kに渡されなかったプロパティの型を取得する必要があります。これはTの中からKに該当するプロパティを除いたものです。

例えば以下のようなTKを渡すことを考えます。

// こっちがT
type Music ={
  name: string,
  artist: string, 
  releaseYear: number
}

// こっちがK
type 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>

4. Kのデフォルト値を設定する

…と思ったらまだエラーが出ています。この型は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だけならいいんですが、他にも自分が理解できていない部分があると怖いです。


Buy Me A Coffeeikuma-tにお恵みを!