こんにちは、PE部の中村です。
今日は、とあるシステム(typescript製)の開発中にAWS SDKのある挙動にハマってしまったので、その話を備忘録として残しておきます。
起きたこと
aws-sdkにおいて、DynamoDBのDocumentClientにArrayを継承した独自クラスを渡すと、 ['hoge', 'fuga', 'piyo']
のようなList型となって欲しいところが { '0': 'hoge', '1': 'fuga', '2': 'piyo' }
のようなMap型となってしまう。
再現の準備
まず、下記のようにArrayを継承したExtendedArrayというクラスをGenericsを使って作成します。そして、Personクラスを作成し、それをまとめるPeopleクラスを、先程のExtendedArrayから継承して作成します。
models.ts
class ExtendedArray<T> extends Array<T> { print(): void { console.log(this.join(', ')); return; } toRawObject(): T[] { return this.map(item => item); } toRawObject2(): T[] { return new Array(...this); } toRawObject3(): T[] { return JSON.parse(JSON.stringify(this)); } } export class Person { firstName = ''; lastName = ''; age = 0; constructor(init: Person) { Object.assign(this, init); } } export class People extends ExtendedArray<Person> {}
そして、実際にDynamoDBにデータを渡していきます。今回は5パターン用意してみました。
5パターンの説明に入る前に一旦ソースコードです。
const main = async (): Promise<void> => { const people = new People( new Person({ age: 5, firstName: 'momotaro', lastName: 'yamada' }), new Person({ age: 6, firstName: 'takashi', lastName: 'suzuki' }), new Person({ age: 7, firstName: 'koichi', lastName: 'sato' }) ); const rawPeople = [ new Person({ age: 5, firstName: 'momotaro', lastName: 'yamada' }), new Person({ age: 6, firstName: 'takashi', lastName: 'suzuki' }), new Person({ age: 7, firstName: 'koichi', lastName: 'sato' }) ]; const testdata: Testcase[] = [ { message: 'extended array', data: people }, { message: 'raw array', data: rawPeople }, { message: 'converted to raw array from extended array using Array.map()', data: people.toRawObject() }, { message: 'converted to raw array from extended array using new Array(...this)', data: people.toRawObject2() }, { message: 'converted to raw array from extended array using JSON.parse(JSON.stringify(this))', data: people.toRawObject3() } ]; for (const td of testdata) { console.log(`Array.isArray(): ${Array.isArray(td.data)}`); console.log(td.data); await putAndView(td); } };
- パターン1: Arrayを拡張したPeopleクラスをそのまま渡す
- パターン2: Peopleクラスは使わずに、Personクラスのインスタンスを素のArrayに入れて渡す
- パターン3: PeopleクラスにPerson[] を返す関数
this.map(item => item)
を実装し、その関数の返り値を渡す - パターン4: PeopleクラスにPerson[] を返す関数
new Array(...this)
を実装し、その関数の返り値を渡す - パターン5: Peopleクラスのインスタンスを 文字列化 -> オブジェクト化する関数
JSON.parse(JSON.stringify(this))
を実装し、その関数の返り値を渡す
結果
結果は以下のようになりました。
- パターン1: Mapになってしまう
- パターン2: Listのまま
- パターン3: Mapになってしまう(!?)
- パターン4: Listのまま
- パターン5: Listのまま
npm run insert > ddb-documentclient-test@0.1.0 insert /Users/xxx/ddb-documentclient-test > npx ts-node src/insertdata.ts Array.isArray(): true People [ Person { firstName: 'momotaro', lastName: 'yamada', age: 5 }, Person { firstName: 'takashi', lastName: 'suzuki', age: 6 }, Person { firstName: 'koichi', lastName: 'sato', age: 7 } ] { "id": "extended array", "data": { "0": { "firstName": "momotaro", "lastName": "yamada", "age": 5 }, "1": { "firstName": "takashi", "lastName": "suzuki", "age": 6 }, "2": { "firstName": "koichi", "lastName": "sato", "age": 7 } } } Array.isArray(): true [ Person { firstName: 'momotaro', lastName: 'yamada', age: 5 }, Person { firstName: 'takashi', lastName: 'suzuki', age: 6 }, Person { firstName: 'koichi', lastName: 'sato', age: 7 } ] { "id": "raw array", "data": [ { "firstName": "momotaro", "lastName": "yamada", "age": 5 }, { "firstName": "takashi", "lastName": "suzuki", "age": 6 }, { "firstName": "koichi", "lastName": "sato", "age": 7 } ] } Array.isArray(): true People [ Person { firstName: 'momotaro', lastName: 'yamada', age: 5 }, Person { firstName: 'takashi', lastName: 'suzuki', age: 6 }, Person { firstName: 'koichi', lastName: 'sato', age: 7 } ] { "id": "converted to raw array from extended array using Array.map()", "data": { "0": { "firstName": "momotaro", "lastName": "yamada", "age": 5 }, "1": { "firstName": "takashi", "lastName": "suzuki", "age": 6 }, "2": { "firstName": "koichi", "lastName": "sato", "age": 7 } } } Array.isArray(): true [ Person { firstName: 'momotaro', lastName: 'yamada', age: 5 }, Person { firstName: 'takashi', lastName: 'suzuki', age: 6 }, Person { firstName: 'koichi', lastName: 'sato', age: 7 } ] { "id": "converted to raw array from extended array using new Array(...this)", "data": [ { "firstName": "momotaro", "lastName": "yamada", "age": 5 }, { "firstName": "takashi", "lastName": "suzuki", "age": 6 }, { "firstName": "koichi", "lastName": "sato", "age": 7 } ] } Array.isArray(): true [ { firstName: 'momotaro', lastName: 'yamada', age: 5 }, { firstName: 'takashi', lastName: 'suzuki', age: 6 }, { firstName: 'koichi', lastName: 'sato', age: 7 } ] { "id": "converted to raw array from extended array using JSON.parse(JSON.stringify(this))", "data": [ { "firstName": "momotaro", "lastName": "yamada", "age": 5 }, { "firstName": "takashi", "lastName": "suzuki", "age": 6 }, { "firstName": "koichi", "lastName": "sato", "age": 7 } ] }
パターン3がMapになってしまうのが意外でした。ログでDynamoDBに渡すオブジェクトを出力していますが、Peopleクラスの情報が保持されたままになっていますね。 this.map(item => item)
とすれば素のArrayが返ると思っていたのですが、そうではなかったようです。まだまだjs力が足りません。
拡張Arrayを保持したままDynamoDBにputするにはパターン4, 5のどちらかを採用すれば良いのですが、パターン5は見た目的にアレなのでパターン4で実装を進めていこうと思います。
(もっと良い方法を知っている人がいればぜひ教えて下さい🥺)