在 TypeScript 中将所有 null 实例转换为未定义的通用方法



我想编写一个函数,将任何 null 对象的属性的所有实例转换为未定义。我的许多对象都有嵌套对象作为值/对象的属性或数组。

在处理这个问题时,我的第一个想法是使用泛型来尝试使用泛型类型捕获每个属性的类型,然后根据需要将该属性的值从 null 转换为未定义。

我希望编写一个足够通用的函数,可以跨我在代码库中拥有的任何不同类型和大小的对象工作。

我一直无法找到一种简单的方法来拥有未知数量的泛型类型,所以我的下一个想法是我必须在任何地方使用任何类型。有没有办法解决这个问题?

我也希望得到一些关于方法/算法本身的建议。我的想法是,我可能需要递归检查每个属性以查看它本身是否是具有子属性的对象,并且我还需要迭代找到的任何可能具有 null 值的数组,或者具有需要递归检查的对象。

我需要解决/回答的问题/问题:

  1. 我应该使用泛型还是任何?
  2. 如果我使用泛型,
  3. 有没有办法在找到新类型时动态分配新的泛型类型?
  4. 打字稿是否有更简单的方法来递归解析对象?

我目前的方法如下:

inputObjectKeys.map(key, index) =>

然后有一个函数,该函数将 null 转换为未定义,忽略非对象类型,如果它是对象,则递归。

我假设我想使用广度优先搜索或深度优先搜索(我倾向于广度优先搜索来完成此特定任务)。我假设因为我需要访问每个节点,我可能会因为内存使用而更好地使用 DFS。

接受的答案实际上没有回答这个问题,因为它被要求一种通用的方式。

下面是一个类似的,也可以正确转换返回类型:

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://stackoverflow.com/a/51458052/6489012
if (obj.constructor.name === "Object") {
for (let key in obj) {
obj[key] = nullsToUndefined(obj[key]) as any;
}
}
return obj as any;
}

功劳归于这位天才的打字:https://github.com/apollographql/apollo-client/issues/2412#issuecomment-755449680

派对有点晚了,但我认为格兰特的回答可以简化一点。这个怎么样:

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

接受的答案不是类型安全的。

这个答案很接近,但不处理嵌套数组内部的null

这会将嵌套对象数组中的null替换为undefined

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

为了递归处理所有类型:

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])
}), {});
}

nullToUndefined()undefinedToNull()的 TypeScript 实现

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游乐场:

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/lodash@4.17.21/lodash.js"></script>



单元测试:

/* 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[Ss]+could not be cloned./
);
// Won't throw if structuredClone() is not used
expect(() => undefinedToNull(fn)).toThrow(
/Uncloneable type: Function|function fn[Ss]+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);
});

好吧,我的答案并不漂亮,但这就是我为我工作的原因:

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

这是我非常紧凑、通用、递归的解决方案! 自我描述:)

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]);
}
}
}
};

您可以使用真实类型的值。使用!!运算符将值转换为真实类型值。

if(!!array[0]) { ... } //If array[0] contains a 'truthy' value, e.g. true, 1, not null, not undefined, etc

相关内容

  • 没有找到相关文章

最新更新