アプリケーションを書いていると、オブジェクトのJSONへの変換や、JSONからのオブジェクトの復元を行うことはよくあります。
ですが、TypeScript上でこうした処理を頻繁に行っていると、「あるオブジェクトや変数の値が、JSONとの間で確実に(一部の情報を失ったりせずに)変換/復元が可能なことを型レベルで保証したい!」と思うことはよくあります。ありますよね?
let food1 = {
name: 'スクランブルエッグ'
, calorie: 230
, stuff: ['卵', '油', '塩']
}
console.log(food1);
// => { name: 'スクランブルエッグ', calorie: 230, stuff: [ '卵', '油', '塩' ] }
console.log(JSON.parse(JSON.stringify(food1)));
// => { name: 'スクランブルエッグ', calorie: 230, stuff: [ '卵', '油', '塩' ] }
// JSONに変換した値を、完全に復元できている
let food2 = {
name: 'スクランブルエッグ'
, calorie: 230
, stuff: ['卵', '油', '塩']
, created: new Date()
, updated: undefined
}
console.log(food2);
// => { name: 'スクランブルエッグ', calorie: 230, stuff: ['卵', '油', '塩'], created: 2019-01-19T14:41:04.353Z, updated: undefined }
console.log(JSON.parse(JSON.stringify(food2)));
// => { name: 'スクランブルエッグ', calorie: 230, stuff: ['卵', '油', '塩'], created: '2019-01-19T14:41:04.353Z' }
// Date型はJSONに変換してしまうと、追加処理なしでは正しく復元できない!
// また、undefinedもJSONに変換した時点で消滅してしまうため、正しく復元できない!
そこで、「JSONとの間で変換/復元が可能な値」を表すJSONSerializable型の定義を作りました。
たった数行の短い型定義なので、特にnpmパッケージなども作っていませんが、有用に思われた方はコピー&ペーストしてご利用ください。
(配列の型定義方法は、GithubにあるTypeScriptプロジェクトのissuesを参考にさせていただきました)
/** JSONとの間で、相互に変換/復元が可能な値 */
type JSONSerializable = JSONSerializablePrimitive | JSONSerializableArray | JSONSerializableObject;
type JSONSerializablePrimitive = null | boolean | string | number;
type JSONSerializableObject = { [key: string]: JSONSerializable };
interface JSONSerializableArray extends Array<JSONSerializable> {
}
さっきのサンプルコードに型を適用すると、このように適切にビルドエラーを出力してくれます。
let food1: JSONSerializable = {
name: 'スクランブルエッグ'
, calorie: 230
, stuff: ['卵', '油', '塩']
}
// => エラーなし
let food2: JSONSerializable = {
name: 'スクランブルエッグ'
, calorie: 230
, stuff: ['卵', '油', '塩']
, created: new Date()
, updated: undefined
}
// => ビルドエラーが発生する
// 型 '{ name: string; calorie: number; stuff: string[]; created: Date; updated: undefined; }' を
// 型 'JSONSerializable' に割り当てることはできません。(以下略)
// 「変換可能なobjectであること(文字列/数値/真偽値/null/配列ではないこと)」を強制したい場合は
// JSONSerializableObject型を使用する
let food3Obj: JSONSerializableObject;
food3Obj = {name: 'たまごやき'}; // OK
food3Obj = 'たまごやき'; // ビルドエラー (object型ではない)
ただし、この型定義の制限として、循環オブジェクトはエラーとして弾けないことにご注意ください。
(もし循環オブジェクトも弾ける方法があれば教えてください)
let node1 = {
parent: null
}
let node2 = {
child: null
}
node1.parent = node2;
node2.child = node1;
let n1: JSONSerializable = node1; // エラーなし
let n2: JSONSerializable = node2; // エラーなし
console.log(JSON.stringify(n1)); // 循環しているためエラー! (TypeError: Converting circular structure to JSON)
(2020/8/1追記)
なお、場合によっては「オブジェクトのプロパティがundefined値を持つことだけは許可したい」という場合もあると思います。(JSONからの変換/復元を介した時に、値がundefinedであるプロパティが削除されていても問題ない場合)
その場合は、代わりに下記のような定義を使用してください。
/** JSONとの間で、相互に変換/復元が可能な値 */
type JSONSerializable = JSONSerializablePrimitive | JSONSerializableArray | JSONSerializableObject;
type JSONSerializablePrimitive = null | boolean | string | number;
type JSONSerializableObject = Partial<{ [key: string]: JSONSerializable }>;
interface JSONSerializableArray extends Array<JSONSerializable> {
}