Generic way to convert all instances of null to undefined in TypeScript
Asked Answered
W

8

16

I am wanting to write a function that converts all instances of a property of any object that is null to undefined. Many of my objects have nested objects as properties or arrays of values/objects.

My first thought when approaching the problem is to use generics to attempt to catch the type of each of the properties with a generic type and then to convert the value of that property from null to undefined as needed.

I am hoping to write a function that is generic enough to work across any of the different types and sized objects I have across my code code base.

I haven't been able to find an easy way to have an unknown number of generic types, so my next thought is that I'll have to use the any type everywhere. Is there a way around this?

I'd also love some advice on the approach/algorithm itself as well. My thought is that I'll probably need to recursively check each property to see if it itself is an object with sub properties, and I'll also need to iterate any arrays found that might have a null value as well, or have an object that will need to be recursively checked as well.

The problems/questions I need to solve/answer:

  1. Should I use Generics or Any?
  2. If i use generics is there a way to dynamically assign new generic types as a new type is found?
  3. Does typescript have an easier way to recursively parse objects?

My current approach is something like:

inputObjectKeys.map(key, index) =>

then have a function that converts null to undefined, ignores non object types, and recurses if its an object.

I'm assuming I'd want to use a Breadth First Search or Depth First Search (I'm leaning towards Breadth First Search for this particular task). I'm assuming since i need to visit every node i might be better off with DFS simply because of the memory usage.

Wheelock answered 16/5, 2018 at 15:32 Comment(2)
This is a bit confusing. Are you just trying to iterate over an object's properties? That's solved in looping through an object (tree) recursively. There are a number of questions about recursively going through objects and setting properties as well. E.g., Javascript object key value coding. Dynamically setting a nested valueArginine
@MikeMcCaughan sorry i tried to provide enough information to make the problem understandable, but i think it had the opposite effect. I want to write a function that will convert any instance of null to undefined in any object i pass to it. The objects can be complex, contain arrays of other objects, or have nested objects. I'll read through the links provided!Wheelock
C
19

The accepted answer is actually not answering the question since it was asked for a generic way.

Here is one that is similar and will also cast the return type properly:

type RecursivelyReplaceNullWithUndefined<T> = T extends null
  ? undefined
  : T extends Date
  ? T
  : {
      [K in keyof T]: T[K] extends (infer U)[]
        ? RecursivelyReplaceNullWithUndefined<U>[]
        : RecursivelyReplaceNullWithUndefined<T[K]>;
    };

export function nullsToUndefined<T>(obj: T): RecursivelyReplaceNullWithUndefined<T> {
  if (obj === null) {
    return undefined as any;
  }

  // object check based on: https://mcmap.net/q/24546/-check-if-a-value-is-an-object-in-javascript
  if (obj.constructor.name === "Object") {
    for (let key in obj) {
      obj[key] = nullsToUndefined(obj[key]) as any;
    }
  }
  return obj as any;
}

Credits go to the typings of this genius: https://github.com/apollographql/apollo-client/issues/2412#issuecomment-755449680

Candelaria answered 4/9, 2021 at 19:37 Comment(0)
R
7

A bit late to the party, but I think Grant's answer can be simplified a bit. How about this one:

function removeNulls(obj: any): any {
    if (obj === null) {
        return undefined;
    }
    if (typeof obj === 'object') {
        for (let key in obj) {
            obj[key] = removeNulls(obj[key]);
        }
    }
    return obj;
}
Recall answered 5/2, 2020 at 22:57 Comment(2)
The answer explicitly asks for a generic way, and this is not generic.Ironworker
How is this not generic? In the sense that it doesn't use Generics? The question clearly states: I am wanting to write a function that converts all instances of a property of any object that is null to undefined. and I am hoping to write a function that is generic enough to work across any of the different types and sized objects I have across my code code base. Both of which are solved by the answer.Recall
A
7

The accepted answer is not type safe.

This answer is close, but doesn't handle null inside of nested arrays.

This will replace null with undefined in nested objects and arrays:

type RecursivelyReplaceNullWithUndefined<T> = T extends null
  ? undefined
  : T extends (infer U)[]
  ? RecursivelyReplaceNullWithUndefined<U>[]
  : T extends Record<string, unknown>
  ? { [K in keyof T]: RecursivelyReplaceNullWithUndefined<T[K]> }
  : T;

export function nullsToUndefined<T>(
  obj: T,
): RecursivelyReplaceNullWithUndefined<T> {
  if (obj === null || obj === undefined) {
    return undefined as any;
  }

  if ((obj as any).constructor.name === 'Object' || Array.isArray(obj)) {
    for (const key in obj) {
      obj[key] = nullsToUndefined(obj[key]) as any;
    }
  }
  return obj as any;
}
Abaca answered 8/6, 2022 at 16:58 Comment(1)
I'd recommend (typeof obj === "object" && obj !== null) instead of (obj as any).constructor.name === 'Object' . See: https://mcmap.net/q/24546/-check-if-a-value-is-an-object-in-javascript. Lodash also has an isObject.Confrere
B
4

In order to recursively handle all types :

function cleanNullToUndefined(obj: any): any {
  if (obj === null) {
      return undefined;

  }
  if (typeof obj !== 'object') {
      return obj;

  }
  if (obj instanceof Array) {
    return obj.map(cleanNullToUndefined);
  }

  return Object.keys(obj).reduce((result, key) => ({
    ...result, 
    [key]: cleanNullToUndefined(obj[key])
  }), {});
}
Blythe answered 7/10, 2020 at 14:24 Comment(0)
C
2

TypeScript implementations of nullToUndefined() and undefinedToNull()

https://gist.github.com/tkrotoff/a6baf96eb6b61b445a9142e5555511a0

/* eslint-disable guard-for-in, @typescript-eslint/ban-types, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-assignment */

import { Primitive } from 'type-fest';

// ["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));
}

JS playground:

function _nullToUndefined(obj) {
    if (obj === null) {
        return undefined;
    }
    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]);
            }
        }
    }
    return obj;
}

function nullToUndefined(obj) {
    return _nullToUndefined(structuredClone(obj));
}

function _undefinedToNull(obj) {
    if (obj === undefined) {
        return null;
    }
    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]);
            }
        }
    }
    return obj;
}

function undefinedToNull(obj) {
    return _undefinedToNull(structuredClone(obj));
}



// Example with a simple object

const obj = {
  keyUndefined: undefined,
  keyNull: null,
  keyString: 'string'
};

const objWithUndefined = nullToUndefined(obj);
console.log(_.isEqual(objWithUndefined, {
  keyUndefined: undefined,
  keyNull: undefined,
  keyString: 'string'
}));

const objWithNull = undefinedToNull(objWithUndefined);
console.log(_.isEqual(objWithNull, {
  keyUndefined: null,
  keyNull: null,
  keyString: 'string'
}));



// Example with a complex object

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' }
  }
};

const jsonWithUndefined = nullToUndefined(json);
console.log(_.isEqual(jsonWithUndefined, {
  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' }
  }
}));

const jsonWithNull = undefinedToNull(jsonWithUndefined);
console.log(_.isEqual(jsonWithNull, {
  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' }
  }
}));
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.js"></script>


Unit tests:

/* 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);
});
Cirsoid answered 16/6, 2023 at 20:31 Comment(1)
had to add a package, otherwise, good answerBelamy
W
0

Well my answer is not pretty, but this is what i got to work for me:

function recurseObject(obj: any) {

    if (obj === null) {
        return undefined;

    } else if (typeof obj !== 'object') {
        return obj;

    } else {

    if (obj instanceof Array) {
        for (let key of obj) {
            recurseObject(key);
    }

    } else {
            for (let key in obj) {

                if (obj[key] === null) {
                    obj[key] = undefined;

                } else if (typeof obj[key] === 'object') {
                    recurseObject(obj[key]);
                }
            }
        }
    }

    return obj;
}
Wheelock answered 16/5, 2018 at 18:10 Comment(0)
D
-1

here is my very compact, generic, recursive solution! self-describing :)

export const nullToUndefined = (obj: object) => {
  if (typeof obj === 'object') {
    for (const [key, val] of Object.entries(obj)) {
      if (val === null) {
        obj[key] = undefined;
      } else {
        nullToUndefined(obj[key]);
      }
    }
  }
};
Doyon answered 24/2, 2023 at 16:9 Comment(2)
Please read "How to Answer" and "Explaining entirely code-based answers". It helps more if you supply an explanation why this is the preferred solution and explain how it works. We want to educate, not just provide code.Sling
the best code does not require additional comments, IMHODoyon
M
-4

You could use truthy-type values. Use the !! operator to convert a value into a truthy type value.

if(!!array[0]) { ... } //If array[0] contains a 'truthy' value, e.g. true, 1, not null, not undefined, etc
Methodist answered 16/5, 2018 at 15:44 Comment(4)
I'm not quite sure i understand what this does? Is it just checking that the value isn't false, undefined, or null?Wheelock
Exactly -- instead of having to check if an object is null or undefined, you can use truthy type values to capture all of the associated values in a single umbrella. What you want is to check value X and if it is null, convert it to undefined and then do some sort of logic. What you CAN do is to check if a value is not truthy (aka falsey) and then do some sort of logic. It removes the need to change all nulls to undefined.Methodist
Is there any chance you could expand your code with a simple example? I'm not quite sure i understand the method fully.Wheelock
This is neither recursive nor generic.Shaddock

© 2022 - 2025 — McMap. All rights reserved.