Skip to content

Commit

Permalink
fix(utils): Ensure dropUndefinedKeys() does not break class instances
Browse files Browse the repository at this point in the history
  • Loading branch information
mydea committed Jan 18, 2024
1 parent 76378af commit d95b132
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 2 deletions.
2 changes: 1 addition & 1 deletion packages/utils/src/is.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export function isPrimitive(wat: unknown): wat is Primitive {
}

/**
* Checks whether given value's type is an object literal
* Checks whether given value's type is an object literal, or a class instance.
* {@link isPlainObject}.
*
* @param wat A value to be checked.
Expand Down
15 changes: 14 additions & 1 deletion packages/utils/src/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ export function dropUndefinedKeys<T>(inputValue: T): T {
}

function _dropUndefinedKeys<T>(inputValue: T, memoizationMap: Map<unknown, unknown>): T {
if (isPlainObject(inputValue)) {
if (isPojo(inputValue)) {
// If this node has already been visited due to a circular reference, return the object it was mapped to in the new object
const memoVal = memoizationMap.get(inputValue);
if (memoVal !== undefined) {
Expand Down Expand Up @@ -263,6 +263,19 @@ function _dropUndefinedKeys<T>(inputValue: T, memoizationMap: Map<unknown, unkno
return inputValue;
}

function isPojo(input: unknown): input is Record<string, unknown> {
if (!isPlainObject(input)) {
return false;
}

try {
const name = (Object.getPrototypeOf(input) as { constructor: { name: string } }).constructor.name;
return !name || name === 'Object';
} catch {
return true;
}
}

/**
* Ensure that something is an object.
*
Expand Down
23 changes: 23 additions & 0 deletions packages/utils/test/object.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,29 @@ describe('dropUndefinedKeys()', () => {
});
});

describe('class instances', () => {
class MyClass {
public a = 'foo';
public b = undefined;
}

test('ignores class instance', () => {
const instance = new MyClass();
const result = dropUndefinedKeys(instance);
expect(result).toEqual({ a: 'foo', b: undefined });
expect(result).toBeInstanceOf(MyClass);
expect(Object.prototype.hasOwnProperty.call(result, 'b')).toBe(true);
});

test('ignores nested instances', () => {
const instance = new MyClass();
const result = dropUndefinedKeys({ a: [instance] });
expect(result).toEqual({ a: [instance] });
expect(result.a[0]).toBeInstanceOf(MyClass);
expect(Object.prototype.hasOwnProperty.call(result.a[0], 'b')).toBe(true);
});
});

test('should not throw on objects with circular reference', () => {
const chicken: any = {
food: undefined,
Expand Down

0 comments on commit d95b132

Please sign in to comment.