Last active
June 18, 2023 08:42
-
-
Save tkrotoff/a6baf96eb6b61b445a9142e5555511a0 to your computer and use it in GitHub Desktop.
Recursively converts all values from null to undefined and vice versa
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable guard-for-in, @typescript-eslint/ban-types, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-assignment */ | |
import { Primitive } from 'type-fest'; | |
// [Generic way to convert all instances of null to undefined in TypeScript](https://stackoverflow.com/q/50374869) | |
// ["I intend to stop using `null` in my JS code in favor of `undefined`"](https://github.com/sindresorhus/meta/discussions/7) | |
// [Proposal: NullToUndefined and UndefinedToNull](https://github.com/sindresorhus/type-fest/issues/603) | |
// Types implementation inspired by | |
// https://github.com/sindresorhus/type-fest/blob/v2.12.2/source/delimiter-cased-properties-deep.d.ts | |
// https://github.com/sindresorhus/type-fest/blob/v2.12.2/source/readonly-deep.d.ts | |
// https://gist.github.com/tkrotoff/a6baf96eb6b61b445a9142e5555511a0 | |
export type NullToUndefined<T> = T extends null | |
? undefined | |
: T extends Primitive | Function | Date | RegExp | |
? T | |
: T extends Array<infer U> | |
? Array<NullToUndefined<U>> | |
: T extends Map<infer K, infer V> | |
? Map<K, NullToUndefined<V>> | |
: T extends Set<infer U> | |
? Set<NullToUndefined<U>> | |
: T extends object | |
? { [K in keyof T]: NullToUndefined<T[K]> } | |
: unknown; | |
export type UndefinedToNull<T> = T extends undefined | |
? null | |
: T extends Primitive | Function | Date | RegExp | |
? T | |
: T extends Array<infer U> | |
? Array<UndefinedToNull<U>> | |
: T extends Map<infer K, infer V> | |
? Map<K, UndefinedToNull<V>> | |
: T extends Set<infer U> | |
? Set<NullToUndefined<U>> | |
: T extends object | |
? { [K in keyof T]: UndefinedToNull<T[K]> } | |
: unknown; | |
function _nullToUndefined<T>(obj: T): NullToUndefined<T> { | |
if (obj === null) { | |
return undefined as any; | |
} | |
if (typeof obj === 'object') { | |
if (obj instanceof Map) { | |
obj.forEach((value, key) => obj.set(key, _nullToUndefined(value))); | |
} else { | |
for (const key in obj) { | |
obj[key] = _nullToUndefined(obj[key]) as any; | |
} | |
} | |
} | |
return obj as any; | |
} | |
/** | |
* Recursively converts all null values to undefined. | |
* | |
* @param obj object to convert | |
* @returns a copy of the object with all its null values converted to undefined | |
*/ | |
export function nullToUndefined<T>(obj: T) { | |
return _nullToUndefined(structuredClone(obj)); | |
} | |
function _undefinedToNull<T>(obj: T): UndefinedToNull<T> { | |
if (obj === undefined) { | |
return null as any; | |
} | |
if (typeof obj === 'object') { | |
if (obj instanceof Map) { | |
obj.forEach((value, key) => obj.set(key, _undefinedToNull(value))); | |
} else { | |
for (const key in obj) { | |
obj[key] = _undefinedToNull(obj[key]) as any; | |
} | |
} | |
} | |
return obj as any; | |
} | |
/** | |
* Recursively converts all undefined values to null. | |
* | |
* @param obj object to convert | |
* @returns a copy of the object with all its undefined values converted to null | |
*/ | |
export function undefinedToNull<T>(obj: T) { | |
return _undefinedToNull(structuredClone(obj)); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable @typescript-eslint/ban-types */ | |
import { expectType } from 'tsd'; | |
import { Opaque } from 'type-fest'; | |
import { nullToUndefined, undefinedToNull } from './ObjectValues'; | |
test('deep clone original value', () => { | |
const obj = { | |
keyUndefined: undefined, | |
keyNull: null, | |
keyString: 'string' | |
}; | |
expect(nullToUndefined(obj)).not.toEqual(obj); | |
expect(undefinedToNull(obj)).not.toEqual(obj); | |
}); | |
test('object', () => { | |
const obj = { | |
keyUndefined: undefined, | |
keyNull: null, | |
keyString: 'string' | |
}; | |
expectType<{ keyUndefined: undefined; keyNull: null; keyString: string }>(obj); | |
const objWithUndefined = nullToUndefined(obj); | |
expect(objWithUndefined).toEqual({ | |
keyUndefined: undefined, | |
keyNull: undefined, | |
keyString: 'string' | |
}); | |
expectType<{ keyUndefined: undefined; keyNull: undefined; keyString: string }>(objWithUndefined); | |
const objWithNull = undefinedToNull(objWithUndefined); | |
expect(objWithNull).toEqual({ | |
keyUndefined: null, | |
keyNull: null, | |
keyString: 'string' | |
}); | |
expectType<{ keyUndefined: null; keyNull: null; keyString: string }>(objWithNull); | |
}); | |
test('array', () => { | |
const arr = [undefined, null, 'string']; | |
expectType<(undefined | null | string)[]>(arr); | |
const arrWithUndefined = nullToUndefined(arr); | |
expect(arrWithUndefined).toEqual([undefined, undefined, 'string']); | |
expectType<(undefined | string)[]>(arrWithUndefined); | |
const arrWithNull = undefinedToNull(arrWithUndefined); | |
expect(arrWithNull).toEqual([null, null, 'string']); | |
expectType<(null | string)[]>(arrWithNull); | |
}); | |
test('function - not supported by structuredClone()', () => { | |
function fn() { | |
return 'Hello, World!'; | |
} | |
expect(fn()).toEqual('Hello, World!'); | |
expectType<Function>(fn); | |
// Won't throw if structuredClone() is not used | |
expect(() => nullToUndefined(fn)).toThrow( | |
/Uncloneable type: Function|function fn[\S\s]+could not be cloned\./ | |
); | |
// Won't throw if structuredClone() is not used | |
expect(() => undefinedToNull(fn)).toThrow( | |
/Uncloneable type: Function|function fn[\S\s]+could not be cloned\./ | |
); | |
}); | |
test('Date', () => { | |
const date = new Date(); | |
const dateISO = date.toISOString(); | |
const dateWithUndefined = nullToUndefined(date); | |
expect(dateWithUndefined.toISOString()).toEqual(dateISO); | |
expectType<Date>(dateWithUndefined); | |
const dateWithNull = undefinedToNull(date); | |
expect(dateWithNull.toISOString()).toEqual(dateISO); | |
expectType<Date>(dateWithNull); | |
}); | |
test('RegExp', () => { | |
const regex = /ab+c/; | |
const regexWithUndefined = nullToUndefined(regex); | |
expect(regexWithUndefined).toEqual(/ab+c/); | |
expectType<RegExp>(regexWithUndefined); | |
const regexWithNull = undefinedToNull(regex); | |
expect(regexWithNull).toEqual(/ab+c/); | |
expectType<RegExp>(regexWithNull); | |
}); | |
test('Set - not supported', () => { | |
// "The only way to "modify" a (primitive) item would be to remove it from the Set and then add the altered item." | |
// https://stackoverflow.com/a/57986103 | |
const set = new Set([undefined, null, 'string']); | |
expectType<Set<undefined | null | string>>(set); | |
const setWithUndefined = nullToUndefined(set); | |
expect([...setWithUndefined]).toEqual([undefined, null, 'string']); | |
expectType<Set<undefined | null | string>>(setWithUndefined); | |
const setWithNull = undefinedToNull(set); | |
expect([...setWithNull]).toEqual([undefined, null, 'string']); | |
expectType<Set<undefined | null | string>>(setWithNull); | |
}); | |
test('Map', () => { | |
const map = new Map([ | |
['keyUndefined', undefined], | |
['keyNull', null], | |
['keyString', 'string'] | |
]); | |
expectType<Map<string, undefined | null | string>>(map); | |
const mapWithUndefined = nullToUndefined(map); | |
expect(Object.fromEntries(mapWithUndefined)).toEqual({ | |
keyUndefined: undefined, | |
// FIXME https://github.com/facebook/jest/issues/13686 | |
//keyNull: undefined, | |
keyNull: null, | |
keyString: 'string' | |
}); | |
expectType<Map<string, undefined | string>>(mapWithUndefined); | |
const mapWithNull = undefinedToNull(map); | |
expect(Object.fromEntries(mapWithNull)).toEqual({ | |
// FIXME https://github.com/facebook/jest/issues/13686 | |
//keyUndefined: null, | |
keyUndefined: undefined, | |
keyNull: null, | |
keyString: 'string' | |
}); | |
expectType<Map<string, null | string>>(mapWithNull); | |
}); | |
test('Opaque type', () => { | |
type UUID = Opaque<string, 'UUID'>; | |
const uuid = '3a34ea98-651e-4253-92af-653373a20c51' as UUID; | |
expectType<UUID>(uuid); | |
const uuidWithUndefined = nullToUndefined(uuid); | |
expect(uuidWithUndefined).toEqual('3a34ea98-651e-4253-92af-653373a20c51'); | |
expectType<UUID>(uuidWithUndefined); | |
const uuidWithNull = undefinedToNull(uuid); | |
expect(uuidWithNull).toEqual('3a34ea98-651e-4253-92af-653373a20c51'); | |
expectType<UUID>(uuidWithNull); | |
}); | |
test('complex JSON', () => { | |
const json = { | |
keyUndefined: undefined, | |
keyNull: null, | |
keyString: 'string', | |
array: [ | |
undefined, | |
null, | |
{ | |
keyUndefined: undefined, | |
keyNull: null, | |
keyString: 'string', | |
array: [undefined, null, { keyUndefined: undefined, keyNull: null, keyString: 'string' }], | |
object: { keyUndefined: undefined, keyNull: null, keyString: 'string' } | |
} | |
], | |
object: { | |
keyUndefined: undefined, | |
keyNull: null, | |
keyString: 'string', | |
array: [undefined, null, { keyUndefined: undefined, keyNull: null, keyString: 'string' }], | |
object: { keyUndefined: undefined, keyNull: null, keyString: 'string' } | |
} | |
}; | |
expectType<{ | |
keyUndefined: undefined; | |
keyNull: null; | |
keyString: string; | |
array: ( | |
| undefined | |
| null | |
| { | |
keyUndefined: undefined; | |
keyNull: null; | |
keyString: string; | |
array: ( | |
| undefined | |
| null | |
| { keyUndefined: undefined; keyNull: null; keyString: string } | |
)[]; | |
object: { keyUndefined: undefined; keyNull: null; keyString: string }; | |
} | |
)[]; | |
object: { | |
keyUndefined: undefined; | |
keyNull: null; | |
keyString: string; | |
array: (undefined | null | { keyUndefined: undefined; keyNull: null; keyString: string })[]; | |
object: { keyUndefined: undefined; keyNull: null; keyString: string }; | |
}; | |
}>(json); | |
const jsonWithUndefined = nullToUndefined(json); | |
expect(jsonWithUndefined).toEqual({ | |
keyUndefined: undefined, | |
keyNull: undefined, | |
keyString: 'string', | |
array: [ | |
undefined, | |
undefined, | |
{ | |
keyUndefined: undefined, | |
keyNull: undefined, | |
keyString: 'string', | |
array: [ | |
undefined, | |
undefined, | |
{ keyUndefined: undefined, keyNull: undefined, keyString: 'string' } | |
], | |
object: { keyUndefined: undefined, keyNull: undefined, keyString: 'string' } | |
} | |
], | |
object: { | |
keyUndefined: undefined, | |
keyNull: undefined, | |
keyString: 'string', | |
array: [ | |
undefined, | |
undefined, | |
{ keyUndefined: undefined, keyNull: undefined, keyString: 'string' } | |
], | |
object: { keyUndefined: undefined, keyNull: undefined, keyString: 'string' } | |
} | |
}); | |
expectType<{ | |
keyUndefined: undefined; | |
keyNull: undefined; | |
keyString: string; | |
array: ( | |
| undefined | |
| { | |
keyUndefined: undefined; | |
keyNull: undefined; | |
keyString: string; | |
array: (undefined | { keyUndefined: undefined; keyNull: undefined; keyString: string })[]; | |
object: { keyUndefined: undefined; keyNull: undefined; keyString: string }; | |
} | |
)[]; | |
object: { | |
keyUndefined: undefined; | |
keyNull: undefined; | |
keyString: string; | |
array: (undefined | { keyUndefined: undefined; keyNull: undefined; keyString: string })[]; | |
object: { keyUndefined: undefined; keyNull: undefined; keyString: string }; | |
}; | |
}>(jsonWithUndefined); | |
const jsonWithNull = undefinedToNull(jsonWithUndefined); | |
expect(jsonWithNull).toEqual({ | |
keyUndefined: null, | |
keyNull: null, | |
keyString: 'string', | |
array: [ | |
null, | |
null, | |
{ | |
keyUndefined: null, | |
keyNull: null, | |
keyString: 'string', | |
array: [null, null, { keyUndefined: null, keyNull: null, keyString: 'string' }], | |
object: { keyUndefined: null, keyNull: null, keyString: 'string' } | |
} | |
], | |
object: { | |
keyUndefined: null, | |
keyNull: null, | |
keyString: 'string', | |
array: [null, null, { keyUndefined: null, keyNull: null, keyString: 'string' }], | |
object: { keyUndefined: null, keyNull: null, keyString: 'string' } | |
} | |
}); | |
expectType<{ | |
keyUndefined: null; | |
keyNull: null; | |
keyString: string; | |
array: (null | { | |
keyUndefined: null; | |
keyNull: null; | |
keyString: string; | |
array: (null | { keyUndefined: null; keyNull: null; keyString: string })[]; | |
object: { keyUndefined: null; keyNull: null; keyString: string }; | |
})[]; | |
object: { | |
keyUndefined: null; | |
keyNull: null; | |
keyString: string; | |
array: (null | { keyUndefined: null; keyNull: null; keyString: string })[]; | |
object: { keyUndefined: null; keyNull: null; keyString: string }; | |
}; | |
}>(jsonWithNull); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment