0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

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

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

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


作ってみた

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()}`},
};

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

これらは配列にもできる

配列版
// 追加情報のデータの型
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コンポーネントを考える

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

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は戻り値の型が厳密でないようだから、型アサーションしている

(一応自前でentriesの型付けができるようにすることも可能)

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

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

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

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

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

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

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

そんなときは、conditional typesを使ってこう定義できる

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]>;
};

このように使える

使用例
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などのライブラリを使ったほうが手っ取り早いかもしれない)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?