TL;DR
Gist、あります。
完成品
// JSONの型定義
type JsonPrimitive = string | number | boolean | null;
type JsonArray = JsonPrimitive[] | JsonObject[];
type JsonObject = {
[key: string]: JsonPrimitive | JsonObject | JsonArray;
};
type Json = JsonPrimitive | JsonArray | JsonObject;
// ユーティリティー型
type Replace<T, Replacer extends keyof T, Replaced> = MappedType<
Pick<T, Exclude<keyof T, Replacer>> & {
[Key in Replacer]: Replaced;
}
>;
type MappedType<T> = { [Key in keyof T]: T[Key] };
type KeysMatching<T, V> = {
[Key in keyof T]-?: T[Key] extends V ? Key : never;
}[keyof T];
type ChildrenTypes<T> = {
[Key in keyof T]-?: T[Key];
}[keyof T];
type RemoveUndefined<T> = Omit<T, KeysMatching<T, undefined>>
// JSON utilities
type asJsonObject<T> = Replace<
RemoveUndefined<T>,
KeysMatching<
RemoveUndefined<T>,
Exclude<ChildrenTypes<RemoveUndefined<T>>, Json>
>,
string
>;
type asJsonArray<T> = T extends (infer U)[] ? asJson<U>[] : asJsonObject<T>;
export type asJson<T> = T extends JsonPrimitive ? T : asJsonArray<T>;
はじめに
Typescriptでフロントエンドを開発していると様々な場面でJSONを扱うことと思います。
しかし、データの中にJSONに対応していない型のデータ(Date型など)はJSONにぶち込むと文字列にされてしまいます。
type Data = {
text: string;
number: number;
null: null;
date: Date;
};
const data: Data = {
text: 'text',
number: 10,
null: null,
date: new Date('2022-01-01'),
};
const result = JSON.parse(JSON.stringify(data));
// => {
// text: 'text',
// number: 10,
// null: null,
// date: '2022-01-01T00:00:00.000Z' <- 文字列(string型)になっている
//}
これではせっかくフロントで型情報を用意しても、Date型を扱っているつもりがいつの間にかstring型になってしまいます。
そこで既存の型情報内のJSONにできない型をすべてstring型にするasJson<T>
みたいな型が欲しくなるところですが、探してもどこにもなかったんで作ってやろうじゃねえかってことです。
JSONの型
mdnでは
JSON は数値、真偽値、文字列、null、配列 (順序付けられた値のシーケンス)、およびこれらの値 (または他の配列やオブジェクト) で構成されるオブジェクト (文字列値マッピング) を表すことができます。 JSON は関数、正規表現、日付などのより複雑なデータ型はネイティブに表現できません。(Date オブジェクトはデフォルトで ISO 形式の日付を含む文字列にシリアライズされるため、情報は完全に失われません。) JSON で追加のデータ型を表す必要がある場合は、シリアライズされる時やデシリアライズされる前に値の変換を行ってください。
とあります。Date型などの複雑なデータ型は文字列にシリアライズされるためのtoJSONメゾットなどで情報を文字列に変換します。
よってJSONでサポートされている型は次のようになります。
- プリミティブ
- string
- number
- boolean
- null
- プリミティブ型のオブジェクト
- プリミティブ型の配列
- 以上の配列またはオブジェクト
JSONのTypescriptにおける型定義
type JsonPrimitive = string | number | boolean | null;
type JsonArray = JsonPrimitive[] | JsonObject[];
type JsonObject = {
[key: string]: JsonPrimitive | JsonObject | JsonArray;
};
type Json = JsonPrimitive | JsonArray | JsonObject;
このJson
という型がJSONの型です。
Typescript2.8で追加されたConditional Typeを使ってJsonPrimitive
,JsonArray
,JsonObject
のそれぞれがぶち込まれたときに分けて型定義していきます。
プリミティブ型が渡されたとき
type asJson<T> = T extends JsonPrimitive ? T : asJsonArray<T>;
配列型が渡されたとき
type asJsonArray<T> = T extends (infer U)[] ? asJson<U>[] : asJsonObject<T>;
この(infer U)
は型推論というもので自動的に配列の型を推論してくれます、とても便利。
オブジェクト型が渡されたとき
こいつが厄介です。今までに出てきた型でない型を持つときにすべてstring型に変換する必要があります。
なのでいくつかのユーティリティー型を作ります。
type Replace<T, Replacer extends keyof T, Replaced> = MappedType<
Pick<T, Exclude<keyof T, Replacer>> & {
[Key in Replacer]: Replaced;
}
>;
type MappedType<T> = { [Key in keyof T]: T[Key] };
type KeysMatching<T, V> = {
[Key in keyof T]-?: T[Key] extends V ? Key : never;
}[keyof T];
type ChildrenTypes<T> = {
[Key in keyof T]-?: T[Key];
}[keyof T];
type RemoveUndefined<T> = Omit<T, KeysMatching<T, undefined>>
ここではReplace<T, Replacer extends keyof T, Replaced>
というユーティリティー型を定義しています。(コードが汚いのは許して)
Replaceは型引数T内のReplacerにマッチするプロパティーの型情報を、一括してReplacedに置き換えることができます。
type hoge = {
bar: string;
baz: string;
qux: string;
};
type fuga = Replace<
hoge,
'bar' | 'qux',
number
>;
// => {
// bar: number;
// baz: string;
// qux: number;
//}
KeysMatching<T, V>
は型引数TのプロパティーのうちVにマッチするプロパティーを返し、ChildrenTypes<T>
は型引数Tのすべてのプロパティーのユニオンを返します。また、JSONはundefined型のプロパティーを削除するので、RemoveUndefined<T>
でTからundefinedなプロパティーを削除します。
これらを使ったasJsonObjectは次のようになります。
type asJsonObject<T> = Replace<
RemoveUndefined<T>,
KeysMatching<
RemoveUndefined<T>,
Exclude<ChildrenTypes<RemoveUndefined<T>>, Json>
>,
string
>;
完成品
完成品はこのようになります。
// JSONの型定義
type JsonPrimitive = string | number | boolean | null;
type JsonArray = JsonPrimitive[] | JsonObject[];
type JsonObject = {
[key: string]: JsonPrimitive | JsonObject | JsonArray;
};
type Json = JsonPrimitive | JsonArray | JsonObject;
// ユーティリティー型
type Replace<T, Replacer extends keyof T, Replaced> = MappedType<
Pick<T, Exclude<keyof T, Replacer>> & {
[Key in Replacer]: Replaced;
}
>;
type MappedType<T> = { [Key in keyof T]: T[Key] };
type KeysMatching<T, V> = {
[Key in keyof T]-?: T[Key] extends V ? Key : never;
}[keyof T];
type ChildrenTypes<T> = {
[Key in keyof T]-?: T[Key];
}[keyof T];
type RemoveUndefined<T> = Omit<T, KeysMatching<T, undefined>>
// JSON utilities
type asJsonObject<T> = Replace<
RemoveUndefined<T>,
KeysMatching<
RemoveUndefined<T>,
Exclude<ChildrenTypes<RemoveUndefined<T>>, Json>
>,
string
>;
type asJsonArray<T> = T extends (infer U)[] ? asJson<U>[] : asJsonObject<T>;
export type asJson<T> = T extends JsonPrimitive ? T : asJsonArray<T>;
ちなみにデータ(型ではない)をJSONに変換する関数は
const convertToJson = <T>(data: T): asJson<T> => JSON.parse(JSON.stringify(data));
このように書くといいと思います。
さいごに
asJson<T>
みたいなユーティリティー型、作りました。もしかしたら対応できてないところとかもあるかもしれないので、使うときは自己責任でお願いします。
もし、これ入れたら動かないぞボケコンニャロというようなことがありましたら頑張ってそれもサポートしたasJsonを作ってください。頭の体操になりますよ。私もやったんだからさ。