このような構造のデータがあるとする
// データの構造
type DataA = {a1: string; a2: number};
type DataB = {b1: boolean; b2: Date};
このデータのそれぞれに型安全に追加情報をつけたい
(例えば、Date型のデータのときはDateにつくgetFullYearなどのメソッドを使いたいとか、数字はプレフィックスをつけて表示したいとか、それぞれで挙動を変えたい。しかし、追加情報のフォーマットは統一したいなど)
作ってみた
// 追加情報のデータの型
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コンポーネントを考える
機能は、データと追加情報を渡すと、データに追加情報を適用して表示するもの
(実際の利用では、これを拡張して、いい感じで情報を表示するコンポーネントを作ることを想定)
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などのライブラリを使ったほうが手っ取り早いかもしれない)