typescriptでObjectのデータに型安全な追加情報をつける方法

このような構造のデータがあるとする

```tsx
// データの構造
type DataA = {a1: string; a2: number};
type DataB = {b1: boolean; b2: Date};
```

このデータのそれぞれに型安全に追加情報をつけたい
（例えば、Date型のデータのときはDateにつくgetFullYearなどのメソッドを使いたいとか、数字はプレフィックスをつけて表示したいとか、それぞれで挙動を変えたい。しかし、追加情報のフォーマットは統一したいなど）

---
作ってみた

```tsx:Object版
// 追加情報のデータの型
type DataOption<T> = {
  [K in keyof T]: {printer: (value: T[K]) => string; label: string};
};

// 追加情報データ
const optionA: DataOption<DataA> = {
  a1: {label: 'A-string', printer: value => value},
  a2: {label: 'A-number', printer: value => `No.${value}`},
};
const optionB: DataOption<DataB> = {
  b1: {label: 'B-boolean', printer: value => `Flag.${value}`},
  b2: {label: 'B-date', printer: value => `Year.${value.getFullYear()}`},
};
```

こんな風に型とデータを作ることができた
データの型をジェネリクスで渡すと、追加情報に型が効いて、安全に設定できる

これらは配列にもできる

```tsx:配列版
// 追加情報のデータの型
type DataOption<T> = {
  [K in keyof T]: {key: K; printer: (value: T[K]) => string; label: string};
}[keyof T][];

// 追加情報データ
const optionA: DataOption<DataA> = [
  {key: 'a1', label: 'A-string', printer: value => value},
  {key: 'a2', label: 'A-number', printer: value => `No.${value}`},
];
const optionB: DataOption<DataB> = [
  {key: 'b1', label: 'B-boolean', printer: value => `Flag.${value}`},
  {key: 'b2', label: 'B-date', printer: value => `Year.${value.getFullYear()}`},
];
```

少しDataOption型が複雑。keyの型を効かせるために、いったんObjectにしてから、パースしている

---

次に、これを実際に利用する関数orコンポーネントを考える

機能は、データと追加情報を渡すと、データに追加情報を適用して表示するもの
（実際の利用では、これを拡張して、いい感じで情報を表示するコンポーネントを作ることを想定）

```tsx:Object版
const format = <T extends object>(
  data: T,
  options: DataOption<T>
): string[] => {
  return (Object.keys(options) as (keyof T)[]).map(key => {
    const opt = options[key];
    return `${opt.label}: ${opt.printer(data[key])}`;
  });
};
```

keysやentriesは戻り値の型が厳密でないようだから、型アサーションしている

（一応[自前]([https://zenn.dev/8times12/articles/ff08c1fac412c9#結論](https://zenn.dev/8times12/articles/ff08c1fac412c9#%E7%B5%90%E8%AB%96))でentriesの型付けができるようにすることも可能）

また、DataOptionが配列版だったときは使うときもうちょっとシンプルにかける

```tsx:配列版
const format = <T extends object>(
  data: T,
  options: DataOption<T>
): string[] => {
  return options.map(opt => `${opt.label}: ${opt.printer(data[opt.key])}`);
};
```

使うときは、以下のように使える

```tsx
format({a1: 'a', a2: 1}, optionA);
format({b1: true, b2: new Date()}, optionB);
```

型は以下の３つのどれかにつければ、残りも推論してくれる

- 関数のジェネリクスにつける（`format<DataA>()`）
- データにつける（`dataA: DataA`）
- オプションにつける（`optionA: DataOption<DataA>`）

---

ついでに、データの型に応じて、追加情報の型を変化させたいとき
（例えば、型が数値のときだけ、最大・最小の追加情報を表示するように型で表現したい）

そんなときは、conditional typesを使ってこう定義できる
```tsx
type StringOptions = {
  label: string;
};

type NumberOptions = {
  min?: number;
  max?: number;
};

type ValueOptions<T> = T extends string
  ? StringOptions
  : T extends number
  ? NumberOptions
  : {};

type DataOption<T> = {
  [K in keyof T]: ValueOptions<T[K]>;
};
```
このように使える
```tsx:使用例
interface User {
  name: string;
  age: number;
  isActive: boolean;
}

const userOptions: DataOption<User> = {
  name: {
    label: "名前",  // stringはlabelだけ
  },
  age: {
    min: 0,        // numberはmin/max
    max: 150,
  },
  isActive: {}     // それ以外は空オブジェクト
};
```

DBを直接書き換える機能で、バリデーションや説明をつけるのに便利
（ただ、場合によっては、zodなどのライブラリを使ったほうが手っ取り早いかもしれない）