Skip to content

Instantly share code, notes, and snippets.

@tkrotoff
Last active June 18, 2023 08:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tkrotoff/a6baf96eb6b61b445a9142e5555511a0 to your computer and use it in GitHub Desktop.
Save tkrotoff/a6baf96eb6b61b445a9142e5555511a0 to your computer and use it in GitHub Desktop.
Recursively converts all values from null to undefined and vice versa
/* 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));
}
/* 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