Skip to content

Commit f145ce6

Browse files
feat: add nullsToUndefined utility function to convert null values to undefined
1 parent 5ffb69a commit f145ce6

File tree

4 files changed

+205
-1
lines changed

4 files changed

+205
-1
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@primoui/utils",
33
"description": "A lightweight set of utilities",
4-
"version": "1.2.2",
4+
"version": "1.2.3",
55
"license": "MIT",
66
"type": "module",
77
"author": {

src/helpers/helpers.test.ts

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
isCuid,
99
isTruthy,
1010
joinAsSentence,
11+
nullsToUndefined,
1112
range,
1213
slugify,
1314
splitArrayIntoChunks,
@@ -193,3 +194,175 @@ describe("tryCatch", () => {
193194
expect(result.error).toEqual(error)
194195
})
195196
})
197+
198+
describe("nullsToUndefined", () => {
199+
it("should convert null to undefined", () => {
200+
expect(nullsToUndefined(null)).toEqual(undefined)
201+
})
202+
203+
it("should preserve undefined values", () => {
204+
expect(nullsToUndefined(undefined)).toEqual(undefined)
205+
})
206+
207+
it("should preserve primitive values", () => {
208+
expect(nullsToUndefined("hello")).toEqual("hello")
209+
expect(nullsToUndefined(42)).toEqual(42)
210+
expect(nullsToUndefined(true)).toEqual(true)
211+
expect(nullsToUndefined(false)).toEqual(false)
212+
expect(nullsToUndefined(0)).toEqual(0)
213+
})
214+
215+
it("should convert null properties in objects to undefined", () => {
216+
const input = {
217+
name: "John",
218+
age: null,
219+
active: true,
220+
description: null,
221+
}
222+
223+
const result = nullsToUndefined(input)
224+
225+
expect(result).toEqual({
226+
name: "John",
227+
age: undefined,
228+
active: true,
229+
description: undefined,
230+
})
231+
})
232+
233+
it("should recursively convert null values in nested objects", () => {
234+
const input = {
235+
user: {
236+
name: "John",
237+
profile: {
238+
bio: null,
239+
avatar: "avatar.jpg",
240+
settings: {
241+
theme: null,
242+
notifications: true,
243+
},
244+
},
245+
},
246+
data: null,
247+
}
248+
249+
const result = nullsToUndefined(input)
250+
251+
expect(result).toEqual({
252+
user: {
253+
name: "John",
254+
profile: {
255+
bio: undefined,
256+
avatar: "avatar.jpg",
257+
settings: {
258+
theme: undefined,
259+
notifications: true,
260+
},
261+
},
262+
},
263+
data: undefined,
264+
})
265+
})
266+
267+
it("should handle empty objects", () => {
268+
const input = {}
269+
const result = nullsToUndefined(input)
270+
expect(result).toEqual({})
271+
})
272+
273+
it("should handle objects with only null values", () => {
274+
const input = {
275+
a: null,
276+
b: null,
277+
c: null,
278+
}
279+
280+
const result = nullsToUndefined(input)
281+
282+
expect(result).toEqual({
283+
a: undefined,
284+
b: undefined,
285+
c: undefined,
286+
})
287+
})
288+
289+
it("should mutate the original object", () => {
290+
const input = {
291+
name: "John",
292+
age: null,
293+
}
294+
295+
const result = nullsToUndefined(input)
296+
297+
// Should return the same reference
298+
expect(result).toBe(input)
299+
// Original object should be mutated
300+
expect(input.age).toEqual(undefined)
301+
})
302+
303+
it("should handle arrays (current behavior: not processed recursively)", () => {
304+
const input = [null, "hello", null, 42]
305+
const result = nullsToUndefined(input)
306+
307+
// Arrays are not plain objects (constructor.name !== "Object"), so they're returned as-is
308+
expect(result).toEqual([null, "hello", null, 42])
309+
expect(result).toBe(input)
310+
})
311+
312+
it("should handle Date objects (current behavior: processed as objects)", () => {
313+
const date = new Date("2023-01-01")
314+
const result = nullsToUndefined(date)
315+
316+
// Date objects have constructor.name !== "Object", so they're returned as-is
317+
expect(result).toEqual(date)
318+
expect(result).toBe(date)
319+
})
320+
321+
it("should handle objects containing arrays", () => {
322+
const input = {
323+
list: [null, "item", null],
324+
name: null,
325+
}
326+
327+
const result = nullsToUndefined(input)
328+
329+
expect(result).toEqual({
330+
list: [null, "item", null], // Array is not processed
331+
name: undefined, // Object property is processed
332+
})
333+
})
334+
335+
it("should handle complex mixed data structures", () => {
336+
const input = {
337+
id: 1,
338+
name: null,
339+
metadata: {
340+
created: "2023-01-01",
341+
updated: null,
342+
tags: ["tag1", null, "tag2"],
343+
config: {
344+
enabled: null,
345+
value: 42,
346+
},
347+
},
348+
stats: null,
349+
}
350+
351+
const result = nullsToUndefined(input)
352+
353+
expect(result).toEqual({
354+
id: 1,
355+
name: undefined,
356+
metadata: {
357+
created: "2023-01-01",
358+
updated: undefined,
359+
tags: ["tag1", null, "tag2"], // Arrays not processed
360+
config: {
361+
enabled: undefined,
362+
value: 42,
363+
},
364+
},
365+
stats: undefined,
366+
})
367+
})
368+
})

src/helpers/helpers.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import slugifyString from "@sindresorhus/slugify"
2+
import { ReplaceNullWithUndefined } from ".."
23

34
/**
45
* A collection of helper functions used throughout the application.
@@ -232,3 +233,23 @@ export const tryCatch = async <T, E = Error>(promise: Promise<T>): Promise<Resul
232233
return { data: null, error: error as E }
233234
}
234235
}
236+
237+
/**
238+
* Converts all null values in an object to undefined.
239+
* @param obj - The object to convert.
240+
* @returns The converted object.
241+
*/
242+
export function nullsToUndefined<T>(obj: T): ReplaceNullWithUndefined<T> {
243+
if (obj === null) {
244+
return undefined as any
245+
}
246+
247+
// object check based on: https://stackoverflow.com/a/51458052/6489012
248+
if (obj?.constructor.name === "Object") {
249+
for (const key in obj) {
250+
obj[key] = nullsToUndefined(obj[key]) as any
251+
}
252+
}
253+
254+
return obj as any
255+
}

src/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,13 @@ export type NestedPartial<T> = {
4444
export type NestedRequired<T> = {
4545
[K in keyof T]-?: T[K] extends object ? NestedRequired<T[K]> : T[K]
4646
}
47+
48+
export type ReplaceNullWithUndefined<T> = T extends null
49+
? undefined
50+
: T extends Date
51+
? T
52+
: {
53+
[K in keyof T]: T[K] extends (infer U)[]
54+
? ReplaceNullWithUndefined<U>[]
55+
: ReplaceNullWithUndefined<T[K]>
56+
}

0 commit comments

Comments
 (0)