オブジェクトのプロパティの一部の型を別の型に変更した型を定義する(TypeScript)

## はじめに
型Aを土台に型Bを作る場合にわかりやすく書けないかなと思い、試しに書いてみました

## 前提
以下のケースを想定しています
- 基になる型と上書きしたい型が存在します
- どちらもオブジェクトです
- あくまで、型定義の話で、実際に値をどう変換するかの部分のベストプラクティスを提示するわけではないです
- TypeScript v5.3.3 の環境で試しています

## 一部のプロパティを別の型に置き換えた型を作成する
プロパティの拡張は許可しませんが、プロパティの型だけを置き換えたい場合の型になります

```typescript:
type Replaced<Base extends object, Replacing extends object> = keyof Replacing extends keyof Base
  ? Omit<Base, keyof Replacing> & Replacing
  : never;
```

### 例1: 一部のパラメータをJSONの文字列で情報を持っていて、オブジェクトにパースしたい
```typescript
interface HasSerializedJsonInterface {
  id: number;
  user: string;
}

interface User {
  name: string;
  age: number;
  loggedIn: boolean;
}

const base: HasSerializedJsonInterface = {
  id: 1,
  user: '{"name": "John", "age": 30, "loggedIn": true}',
};

const deserializeUser = (
  base: HasSerializedJsonInterface,
): Replaced<HasSerializedJsonInterface, {user: User}> => {
  const {user, ...rest} = base;
  return {
    ...rest,
    user: JSON.parse(user),
  };
};

console.log(base); // { id: 1, user: '{"name": "John", "age": 30, "loggedIn": true}' }
console.log(deserializeUser(base)); // { id: 1, user: { name: 'John', age: 30, loggedIn: true } }
```

### 例2: 一部のパラメータをJSONの文字列にパースしたい

```typescript
interface HasDeserializedJsonInterface {
  id: number;
  user: User;
}

interface User {
  name: string;
  age: number;
  loggedIn: boolean;
}

const base: HasDeserializedJsonInterface = {
  id: 1,
  user: {name: 'John', age: 30, loggedIn: true},
};

const serializeUser = (
  base: HasDeserializedJsonInterface,
): Replaced<HasDeserializedJsonInterface, {user: string}> => {
  const {user, ...rest} = base;
  return {
    ...rest,
    user: JSON.stringify(user),
  };
};

console.log(base); // { id: 1, user: { name: 'John', age: 30, loggedIn: true } }
console.log(serializeUser(base)); // { id: 1, user: '{"name":"John","age":30,"loggedIn":true}' }
```

## 一部のプロパティを別の型に置き換えつつ、新しいプロパティを追加した型を作成する
先ほどの`Replaced`のケースでは、プロパティの拡張を許可していませんでした
今度は、基になる型で存在しないプロパティがあった場合においても、型の拡張ができるようにしてみます

```typescript
type Merged<Base extends object, Merging extends object> = keyof Merging extends keyof Base
  ? Omit<Base, keyof Merging> & Merging
  : Base & Merging;
```

↓ type-challengesの中級問題で同じ用途のものがありました

https://github.com/type-challenges/type-challenges/tree/main/questions/00599-medium-merge


### 例1:　一部のパラメータをJSONの文字列で情報を持っていて、オブジェクトにパースし、追加のプロパティを設定したい
```typescript
interface HasSerializedJsonInterface {
  id: number;
  user: string;
}

interface User {
  name: string;
  age: number;
  loggedIn: boolean;
}

interface UserAdditionalProperties {
  isAdmin: boolean;
}

const base: HasSerializedJsonInterface = {
  id: 1,
  user: '{"name": "John", "age": 30, "loggedIn": true}',
};

const deserializeUser = (
  base: HasSerializedJsonInterface,
  isAdmin: boolean,
): Merged<
  HasSerializedJsonInterface,
  {user: User; additionalProperties: UserAdditionalProperties}
> => {
  const {user, ...rest} = base;
  return {
    ...rest,
    user: JSON.parse(user),
    additionalProperties: {isAdmin},
  };
};

console.log(base); // { id: 1, user: '{"name": "John", "age": 30, "loggedIn": true}' }
console.log(deserializeUser(base, true)); // { id: 1, user: { name: 'John', age: 30, loggedIn: true }, additionalProperties: { isAdmin: true } }
console.log(deserializeUser(base, false)); // { id: 1, user: { name: 'John', age: 30, loggedIn: true }, additionalProperties: { isAdmin: false } }
```

## さいごに
TypeScriptの型定義に慣れていないからか難しく捉えがちですが、整理してみると意外とシンプルになるものですね（奥深い）

## 参考
https://github.com/type-challenges/type-challenges/tree/main/questions/00116-medium-replace

https://github.com/type-challenges/type-challenges/tree/main/questions/00599-medium-merge

https://qiita.com/NPG418/items/44eb13016c13a4708254