type-challenges の Readonly に挑戦した

# はじめに

以前投稿した [Pick](https://qiita.com/yamataikoku2001/items/3b9c505c38e1f21d6971) の問題に続けて、Readonly の問題にも挑戦してみたので、思考の整理も兼ねて備忘録として残したいと思います。

# 問題内容

詳しい問題内容は以下から参照できます。

https://github.com/type-challenges/type-challenges/blob/main/questions/00007-easy-readonly/README.ja.md

## 問題

組み込みの型ユーティリティ`Readonly<T>`を使用せず、`T` のすべてのプロパティを読み取り専用にする型を実装します。実装された型のプロパティは再割り当てできません。

例えば：

```tsx
interface Todo {
  title: string
  description: string
}

const todo: MyReadonly<Todo> = {
  title: "Hey",
  description: "foobar"
}

todo.title = "Hello" // Error: cannot reassign a readonly property
todo.description = "barFoo" // Error: cannot reassign a readonly property
```

## そもそもユーティリティ型 `ReadOnly<T>` とは何か

ユーティリティ型 `Readonly<T>` は「**全プロパティを読み取り専用にする**」型です。

全プロパティに対して、`readonly` 修飾子をつけるということです。

ここでいう「全プロパティ」というのは、オブジェクト型 `T` の直下のプロパティのみを表しています。つまり、直下のプロパティの下にネストされているオブジェクトには、`readonly` が適用されないということです。

具体的に表すと以下のようになります。

```tsx
type Person = {
  surname: string;
  middleName?: string;
  givenName: string;
  birthday: {
    year: number;
    month: number;
    day: number;
  };
};

type ReadonlyPerson = Readonly<Person>;

type ReadonlyPerson = {
  readonly surname: string;
  readonly middleName?: string;
  readonly givenName: string;
  readonly birthday: {
    year: number;     // readonly ではない
    month: number;    // readonly ではない
    day: number;      // readonly ではない
  };
};
```

直下のプロパティにのみ、`readonly` がついていることがわかると思います。

# 回答

以下のような回答を書きました。

```tsx
type MyReadonly<T> = { readonly [K in keyof T]: T[K] }
```

# 分解してみていく

## `readonly [K in keyof T]`

### `K in keyof T`

これは Mapped Types（マップ型）です。既存の型 `T` を元に新しい型 `K` を作成しています。

`keyof` はオブジェクトの型からプロパティ名を返す演算子なので、オブジェクト `T` の プロパティ名が順番に `K` に入っていくイメージです。

`keyof T` は具体的には以下のような結果が得られます。

注意しておきたいのは `keyof` で取れるプロパティ名は**1階層目（オブジェクト直下のプロパティ）**だけということです。

```tsx
interface Person = {
  surname: string;
  middleName?: string;
  givenName: string;
  birthday: {
    year: number;
    month: number;
    day: number;
  };
};

// keyof Person の結果
type PersonKeys = keyof Person; // "surname" | "middleName" | "givenName" | "birthday"
```

`keyof Person` の結果を `K` にマッピングしているということがイメージできましたでしょうか。

## `T[K]`

これはオブジェクト `T` から `K` という名前のプロパティの型を取り出しています。

具体的には

`T = Person`、`K = "surname"` とすると、`Person["surname"]`なので、`string` が取り出されるということです。

# 気をつけたいこと

ユーティリティ型 `Readonly<T>` の所でも少し触れていますが、`Readonly<T>` は再帰的ではない（オブジェクト直下のプロパティにのみ `readonly` をつける）ので、今回作成した `MyReadonly<T>` も当たり前ですが、再帰的ではないということです。

実際に `Person` インターフェースに `MyReadonly` を適用した型で定義したオブジェクトを用意して確認してみます。

```tsx
interface Person = {
  surname: string;
  middleName?: string;
  givenName: string;
  birthday: {
    year: number;
    month: number;
    day: number;
  };
};

const person: MyReadonly<Person> = {
	surname: "surname",
	middleName: "middlename",
	givenName: "givenname",
	birthday: {
		year: 2024,
		month: 10,
		day: 21,
	},
}

person.surname = "Nick" // コンパイルエラーが起きる
person.birthday.year = 2022 // 書き込みでき、コンパイルエラー起きない
```

このように再帰的ではないので、2階層目以降は readonly が適用されていないことを理解しておきましょう。

# 最後に

以前の Pick 型よりは簡単なような気がしました。

もし間違いなどあれば、ご指摘いただけるとありがたいです。

# 参考文献

https://typescriptbook.jp/reference/values-types-variables/object/readonly-property

https://typescriptbook.jp/reference/type-reuse/utility-types/readonly

https://github.com/type-challenges/type-challenges/blob/main/questions/00007-easy-readonly/README.ja.md