😇
lodashのcloneDeepWithがわからなすぎてChatGPTと壁打ちしまくった
概要
- 同僚のコードをリファクタリングするタイミングで、lodashのcloneDeepWithに出会う
- JavaScript Objectのvalueにundefinedがいたらnullに置き換える関数で使われていた
- テストコードを書いて動かしてみたが、そもそもcloneDeepWithの挙動がよくわからなすぎて沼った
- ChatGPTにアドバイスを求めましたが、「お前の言っていることよくわかん!!」となり、だいぶ混乱した
リファクタ前のコード
試しに書いたテストコード
import { describe, test, expect } from 'vitest'
import { replaceUndefinedToNull } from '../src/index'
describe('テストコード!', () => {
test('undefined -> nullに変換する', () => {
const originalJson = {
foo: 'bar',
baz: undefined,
nested: {
prop: undefined,
arr: [1, 2, undefined],
hoge: {
arr: [1, 2, 3],
fuga: undefined
}
}
}
const expected = {
foo: 'bar',
baz: null,
nested: {
prop: null,
arr: [1, 2, null],
hoge: {
arr: [1, 2, 3],
fuga: null
}
}
}
const actual = replaceUndefinedToNull(originalJson)
expect(actual).toEqual(expected)
expect(actual).not.toBe(originalJson)
expect(actual.nested).not.toBe(originalJson.nested)
expect(actual.nested.arr).not.toBe(originalJson.nested.arr)
})
test('undefinedがない場合、特に変換はしない', () => {
const originalJson = {
foo: 'bar',
baz: 123,
nested: {
prop: 'value',
arr: [1, 2, 3]
}
}
const actual = replaceUndefinedToNull(originalJson)
expect(actual).toBe(originalJson)
})
})
replaceUndefinedToNull
の実装(初期)
import _ from 'lodash'
const replaceUndefinedToNull = (value: unknown): unknown => {
// debugのためstdoutしてみる
console.log(value)
return _.isObject(value) ? undefined : value
}
export const formatClientOfferJson = (originalJson: unknown) => {
return _.cloneDeepWith(originalJson, replaceUndefinedWithNull)
}
結果
どっちのテストケースもエラー. 1つ目のテストの挙動もこんな感じ
{
foo: 'bar',
baz: undefined,
nested: {
prop: undefined,
arr: [ 1, 2, undefined ],
hoge: { arr: [Array], fuga: undefined }
}
}
bar
undefined
{
prop: undefined,
arr: [ 1, 2, undefined ],
hoge: { arr: [ 1, 2, 3 ], fuga: undefined }
}
undefined
[ 1, 2, undefined ]
1
2
undefined
{ arr: [ 1, 2, 3 ], fuga: undefined }
[ 1, 2, 3 ]
1
2
3
undefined
????????
- まず入力したObjectが全部もらうのもよくわからない
- そのあと再帰的に辿ってそうな挙動ではある
何も変更しないで返してみる
const replaceUndefinedToNull = (value: unknown): unknown => {
console.log(value)
return value
}
{
foo: 'bar',
baz: undefined,
nested: {
prop: undefined,
arr: [ 1, 2, undefined ],
hoge: { arr: [Array], fuga: undefined }
}
}
?????????????
- undefinedを返さないと再帰しないということか
- undefined以外を返すとそこで止まりそうな雰囲気
だいぶ混乱しました
- そもそもコールバックの引数のvalueはとあるkeyのvalueと思ってた
- この認識を修正するのにだいぶ混乱した
- GitHubで実装を確認する
- なるほど、わからん
- ChatGPTにお前の言っていることはわからない!って返すと謝罪して返すが、今見直すとハルシネーションっぽい回答するので、それもありだいぶ混乱した
とりあえず色々叩いた結果、「返り値で再帰するかどうか判断している」ということで僕の認識が改まりました
- undefined or nullだと再帰する
- それ以外はそのまま返るので何もしない
多分こうです。間違ってたらコメントください(修正します)
リファクタする
というわけでこうなりました
const replaceUndefinedToNull = (value: unknown): unknown => {
if (value === undefined) {
// undefinedであればnullに変換する
return null
}
if (!_.isObject(value)) {
return value
}
if (_.isArray(value)) {
// リストの中にundefinedがあればnullに置き換える
return (value as Array<unknown>).map((item: unknown) => (item === undefined ? null : item))
} else {
// Recordの中にundefinedなvalueがあれば再帰したいのでundefinedを返す
return _.some(value as Record<string, unknown>, v => v === undefined) ? undefined : value
}
}
// ...
✓ テストコード! (2)
✓ undefined -> nullに変換する
✓ undefinedがない場合、特に変換はしない
// ...
よしっ!!(猫)
雑感
このまま素通りすると、3ヶ月後同じことでハマりそうだったのでメモしました
参考URL
実装
Discussion