😇

lodashのcloneDeepWithがわからなすぎてChatGPTと壁打ちしまくった

2024/02/14に公開

概要

  • 同僚のコードをリファクタリングするタイミングで、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にお前の言っていることはわからない!って返すと謝罪して返すが、今見直すとハルシネーションっぽい回答するので、それもありだいぶ混乱した

https://twitter.com/aipacommander/status/1757595925873324032

とりあえず色々叩いた結果、「返り値で再帰するかどうか判断している」ということで僕の認識が改まりました

  • 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

https://lodash.com/docs/4.17.15#cloneDeepWith

https://wood-roots.com/web/javascript/2703

実装

https://github.com/lodash/lodash/blob/c7c70a7da5172111b99bb45e45532ed034d7b5b9/src/cloneDeepWith.ts#L35
https://github.com/lodash/lodash/blob/c7c70a7da5172111b99bb45e45532ed034d7b5b9/src/.internal/baseClone.ts#L157

CBcloud Tech Blog

Discussion