diff --git a/docs/pages/guard/_meta.json b/docs/pages/guard/_meta.json index a0efc5f..4527dd1 100644 --- a/docs/pages/guard/_meta.json +++ b/docs/pages/guard/_meta.json @@ -6,6 +6,7 @@ "is-not-null": "isNotNull", "is-null": "isNull", "is-object": "isObject", + "is-plain-object": "isPlainObject", "is-promise": "isPromise", "is-set": "isSet", "is-string": "isString", diff --git a/docs/pages/guard/is-object.mdx b/docs/pages/guard/is-object.mdx index 20fb7da..449d321 100644 --- a/docs/pages/guard/is-object.mdx +++ b/docs/pages/guard/is-object.mdx @@ -1,6 +1,6 @@ # isObject -Check whether the given value is an object +Check whether the given value is an object. ### Import @@ -17,21 +17,21 @@ function isObject(x: unknown): x is ObjectType; ### Examples ```typescript copy -isObject('') // false -isObject('hello world') // false -isObject(null) // false -isObject(true) // false -isObject(undefined) // false -isObject(NaN) // false -isObject(0) // false -isObject(isObject) // false -isObject(false) // false -isObject([]) // false -isObject([2]) // false -isObject(new Map()) // false -isObject(new Set()) // false -isObject(new RegExp('foo')) // false -isObject({}) // true -isObject({ a: 2 }) // true -isObject({ 2: 'a' }) // true +isObject('') // false +isObject('hello world') // false +isObject(null) // false +isObject(true) // false +isObject(undefined) // false +isObject(NaN) // false +isObject(0) // false +isObject(isObject) // false +isObject(false) // false +isObject([]) // false +isObject([2]) // false +isObject(new Map()) // true +isObject(new Set()) // true +isObject(new RegExp('foo')) // true +isObject({}) // true +isObject({ a: 2 }) // true +isObject({ 2: 'a' }) // true ``` diff --git a/docs/pages/guard/is-plain-object.mdx b/docs/pages/guard/is-plain-object.mdx new file mode 100644 index 0000000..325c12a --- /dev/null +++ b/docs/pages/guard/is-plain-object.mdx @@ -0,0 +1,37 @@ +# isPlainObject + +Chec whether the given value was created by the Object constructor, or `Object.create(null)`. + +### Import + +```typescript copy +import { isPlainObject } from '@fullstacksjs/toolbox'; +``` + +### Signature + +```typescript copy +function isPlainObject(x: unknown): x is ObjectType; +``` + +### Examples + +```typescript copy +isPlainObject('') // false +isPlainObject('hello world') // false +isPlainObject(null) // false +isPlainObject(true) // false +isPlainObject(undefined) // false +isPlainObject(NaN) // false +isPlainObject(0) // false +isPlainObject(isPlainObject) // false +isPlainObject(false) // false +isPlainObject([]) // false +isPlainObject([2]) // false +isPlainObject(new Map()) // false +isPlainObject(new Set()) // false +isPlainObject(new RegExp('foo')) // false +isPlainObject({}) // true +isPlainObject({ a: 2 }) // true +isPlainObject({ 2: 'a' }) // true +``` diff --git a/src/guards/isObject.spec.ts b/src/guards/isObject.spec.ts index 41f1e81..835722e 100644 --- a/src/guards/isObject.spec.ts +++ b/src/guards/isObject.spec.ts @@ -13,9 +13,9 @@ describe('isObject', () => { { x: false, expected: false }, { x: [], expected: false }, { x: [2], expected: false }, - { x: new Map(), expected: false }, - { x: new Set(), expected: false }, - { x: new RegExp('foo'), expected: false }, + { x: new Map(), expected: true }, + { x: new Set(), expected: true }, + { x: new RegExp('foo'), expected: true }, { x: {}, expected: true }, { x: { a: 2 }, expected: true }, { x: { 2: 'a' }, expected: true }, diff --git a/src/guards/isObject.ts b/src/guards/isObject.ts index a5d7c4a..8336dc6 100644 --- a/src/guards/isObject.ts +++ b/src/guards/isObject.ts @@ -1,5 +1,4 @@ -import type { ObjectType } from '../types/types'; -import { getTypeOf } from '../types'; +import type { ObjectType } from '../types'; /** * Check whether the given value is an object @@ -9,23 +8,23 @@ import { getTypeOf } from '../types'; * * @example * - * isObject('') // false - * isObject('hello world') // false - * isObject(null) // false - * isObject(true) // false - * isObject(undefined) // false - * isObject(NaN) // false - * isObject(0) // false - * isObject(isObject) // false - * isObject(false) // false - * isObject([]) // false - * isObject([2]) // false - * isObject(new Map()) // false - * isObject(new Set()) // false - * isObject(new RegExp('foo')) // false - * isObject({}) // true - * isObject({ a: 2 }) // true - * isObject({ 2: 'a' }) // true + * isObject('') // false + * isObject('hello world') // false + * isObject(null) // false + * isObject(true) // false + * isObject(undefined) // false + * isObject(NaN) // false + * isObject(0) // false + * isObject(isObject) // false + * isObject(false) // false + * isObject([]) // false + * isObject([2]) // false + * isObject(new Map()) // false + * isObject(new Set()) // false + * isObject(new RegExp('foo')) // false + * isObject({}) // true + * isObject({ a: 2 }) // true + * isObject({ 2: 'a' }) // true */ export const isObject = (x: unknown): x is ObjectType => - getTypeOf(x) === 'object' && x !== null; + typeof x === 'object' && !Array.isArray(x) && x !== null; diff --git a/src/guards/isPlainObject.spec.ts b/src/guards/isPlainObject.spec.ts new file mode 100644 index 0000000..b6acb93 --- /dev/null +++ b/src/guards/isPlainObject.spec.ts @@ -0,0 +1,30 @@ +import { isPlainObject } from './isPlainObject.ts'; + +describe('isPlainObject', () => { + const cases = [ + { x: '', expected: false }, + { x: 'hello world', expected: false }, + { x: null, expected: false }, + { x: true, expected: false }, + { x: undefined, expected: false }, + { x: NaN, expected: false }, + { x: 0, expected: false }, + { x: isPlainObject, expected: false }, + { x: false, expected: false }, + { x: [], expected: false }, + { x: [2], expected: false }, + { x: new Map(), expected: false }, + { x: new Set(), expected: false }, + { x: new RegExp('foo'), expected: false }, + { x: {}, expected: true }, + { x: { a: 2 }, expected: true }, + { x: { 2: 'a' }, expected: true }, + ]; + + it.each(cases)( + 'should return $expected for $x as input', + ({ x, expected }) => { + expect(isPlainObject(x)).toBe(expected); + }, + ); +}); diff --git a/src/guards/isPlainObject.ts b/src/guards/isPlainObject.ts new file mode 100644 index 0000000..9ef6b52 --- /dev/null +++ b/src/guards/isPlainObject.ts @@ -0,0 +1,22 @@ +import type { ObjectType } from '../types/types'; + +function isObject(o: unknown): o is ObjectType { + return Object.prototype.toString.call(o) === '[object Object]'; +} + +export function isPlainObject(o: unknown): o is ObjectType { + if (!isObject(o)) return false; + + const ctor = o.constructor; + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (ctor == null) return true; + + const prototype = ctor.prototype; + if (!isObject(prototype)) return false; + + // If constructor does not have an Object-specific method + if (!prototype.hasOwnProperty('isPrototypeOf')) return false; + + return true; +} diff --git a/src/object/merge.ts b/src/object/merge.ts index 4040923..e3e8136 100644 --- a/src/object/merge.ts +++ b/src/object/merge.ts @@ -1,4 +1,5 @@ -import { isObject, isMap, isSet } from '../guards'; +import { isMap, isSet } from '../guards'; +import { isPlainObject } from '../guards/isPlainObject'; import type { Merge, ObjectType } from '../types/types'; interface ComposerArguments { @@ -19,7 +20,7 @@ function defaultComposer({ path, extract, }: ComposerArguments): unknown { - if (isObject(v1) && isObject(v2)) return extract(v1, v2, path); + if (isPlainObject(v1) && isPlainObject(v2)) return extract(v1, v2, path); else if (Array.isArray(v1) && Array.isArray(v2)) return [...v1, ...v2]; else if (isSet(v1) && isSet(v2)) return new Set([...v1, ...v2]); else if (isMap(v1) && isMap(v2)) return new Map([...v1, ...v2]);