Repository: remeda/remeda
Pull request #436 (2023-12-07 14:32:12)

feat(isObject): Replaced with isObjectType and isPlainObject
Fixes: #414

---

Make sure that you:

- [x] Typedoc added for new methods and updated for changed
- [x] Tests added for new methods and updated for changed
- [x] New methods added to `src/index.ts`
- [x] New methods added to `mapping.md`

---

<details><summary>We use semantic PR titles to automate the release process!</summary>

https://conventionalcommits.org

PRs should be titled following using the format: `< TYPE >(< scope >)?: description`

### Available Types:

- `feat`: new functions!
- `fix`: changes to function implementations that fix a bug where a function produces the wrong _runtime_ results.
- `type`: type-only changes (params, overloading, return types, etc...)
- `perf`: changes to function implementations that improve a functions _runtime_ performance.
- `refactor`: changes to function implementations that are neither `fix` nor `perf`
- `test`: tests-only changes (transparent to users of the function).
- `docs`: changes to the documentation of a function **or the documentation site**.
- `build`, `ci`, `style`, `chore`, and `revert`: are only relevant for the internals of the library.

For scope put the name of the function you are working on (either new or
existing).

</details>


codesandbox-ci[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/remeda/remeda/pr/436/builds/447104) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 42c05c3485c42a712a2a0dbb743e0926d6cdbc26:

|Sandbox| Source |
|--|--|
|[remeda-example-vanilla](https://codesandbox.io/s/remeda-example-vanilla-d7qg6v)| Configuration |

stwlam: How does it cope with something like the following?

```ts
const arr: Record<number, string> = ["a", "b", "c"];
if (R.isPlainObject(arr)) {
  console.log("a plain object"); // Probably not this
} else if (Array.isArray(arr)) {
  console.log("an array");  // Probably this
}
```
eranhirsch: I'm not sure I understand the question, the prototype of `["a", "b", "c"]` is not `Object.prototype` or `null` so it would return false on `isPlainObject`
stwlam: > I'm not sure I understand the question, the prototype of `["a", "b", "c"]` is not `Object.prototype` or `null` so it would return false on `isPlainObject`

The guard seems like it would exclude the tested variable from being a `Record`, which seems like it would become `never` after that.
eranhirsch: I don't understand what you mean. Can you provide a specific bug where the typing wasn't correct?

The guard looks to be working as expected: https://www.typescriptlang.org/play?#code/KYDwDg9gTgLgBDAnmYcByBDKUIHdgAmAKhADxEA0cAQhgM7AB8cAvHAKIgxQYDGM5KrQbNQMYADsCdOBOAA3YFABQcOAH4a9YKrgAuDlx79BWkQG5ly0JFhwAZgFcJ-AJYQJcV3QAKAGwxXCQB5ACMAK2ATIkYACl0CDBgMAyI4AB84ACUo6AJSHxwUWEQAaWBEKmcAawk8CUZlAEoDROSvGUxsPEISUxzePIKipSRyyrgautwG5gBvXVd7OFikFAhltow4AEIWNgByCAiomAOMzK3WfdlHPz8muAW1NShgGEcoT3sMPwZLNQAXysakGEjo8DAOBgEFYcDCkX4ADoAObvQoQGFrYDBeyxLZNAFwN4fL5wKGY2H7NgSO5+C7k6FUm4I05IilY5DASzA5RgiFwLBQAwDIa0gC2oSUVAhUCCKOYbAA2gAiDAqqgq0IauAq3gqgC6liWK28-kCIRO-FiQqaj2ecH5ED8wCRfggKJt2EJcAA9L64BjQhhQn5ELJMQgABbeXT+l4Jl4APXUykBcGAf1QJtiAEFsBhEEjvPmeIgvVA7U9dE6XW6PRWfX6A0GQ2Ho7G1PHEwmU2mgA

The problem is not with our guard; it's with `Array.isArray` cannot narrow a Record<number, string> to an array; that's outside the scope of this change.
stwlam: It is true that not all `Record<number, T>`s are `Array<T>`s, but it is also the case that all `Array<T>`s are `Record<number, T>`s. The `isPlainObject` return predicate isn't stating some variable is a "plain object" in some sense: it's reporting that the passed argument is a `Record`. Many non-plain objects are also `Record`s, so the guard is prone to returning false negatives. As far as I'm aware, typescript has no type for a plain object, so any implementation wouldn't be to accurately type-guard.
eranhirsch: You are looking at it from the wrong side. isPlainObject is not called isRecord on purpose. It doesn't try to detect anything that could implement a Record interface; it narrows down whatever **is** a plain object, and those objects should always extend a `Record<PropertyKey, unknown>`. I couldn't find any example of a plain object not also being some sort of Record.

The runtime definition of a plain object is not new; we copied it as-is from lodash.

I'm still trying to understand what bug you are reporting; it would be helpful if you could draft a test case showing the bug, and if you have suggestions for fixes to the types, that would be great, too.

Patch:
diff --git a/mapping.md b/mapping.md
index db83649..de663b2 100644
--- a/mapping.md
+++ b/mapping.md
@@ -47,6 +47,8 @@ documentation when migrating._
 | `invert`            | `invert`            | `invertObj`         |
 | `isEmpty`           | `isEmpty`           | `isEmpty`           |
 | `isNil`             | `isNil`             | `isNil`             |
+| `isObjectType`      | `isObjectLike`      |                     |
+| `isPlainObject`     | `isPlainObject`     |                     |
 | `isSymbol`          | `isSymbol`          |                     |
 | `join`              | `join`              | `join`              |
 | `last`              | `last`              | `last`              |
diff --git a/src/_types.ts b/src/_types.ts
index 030d77f..2af773b 100644
--- a/src/_types.ts
+++ b/src/_types.ts
@@ -38,6 +38,16 @@ export type ReadonlyTuple<
     ReadonlyArray<Element> // It's not fixed length.
   : BuildTupleHelper<Element, Length, []>; // Otherwise it is a fixed length tuple.
 
+/**
+ * An extension of Extract for type predicates which falls back to the base
+ * in order to narrow the `unknown` case.
+ * @example
+ *   function isMyType<T>(data: T | MyType): data is NarrowedTo<T, MyType> { ... }
+ */
+export type NarrowedTo<T, Base> = Extract<T, Base> extends never
+  ? Base
+  : Extract<T, Base>;
+
 type BuildTupleHelper<
   Element,
   Length extends number,
diff --git a/src/index.ts b/src/index.ts
index cbc5246..5b71ccb 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -52,9 +52,11 @@ export * from './isNonNull';
 export * from './isNot';
 export * from './isNumber';
 export * from './isObject';
+export * from './isObjectType';
+export * from './isPlainObject';
 export * from './isPromise';
-export * from './isSymbol';
 export * from './isString';
+export * from './isSymbol';
 export * from './isTruthy';
 export * from './join';
 export * from './keys';
diff --git a/src/isObject.ts b/src/isObject.ts
index 2a65e42..d148e64 100644
--- a/src/isObject.ts
+++ b/src/isObject.ts
@@ -8,8 +8,12 @@ type DefinitelyObject<T> = Exclude<
 > extends never
   ? Record<string, unknown>
   : Exclude<Extract<T, object>, Array<any> | Function | ReadonlyArray<any>>;
+
 /**
  * A function that checks if the passed parameter is of type Object and narrows its type accordingly
+ *
+ * ! **DEPRECATED: The semantics of this guard are confusing. It excludes Arrays but does not exclude other complex objects like Classes or built-in objects (such as Date, Promise, Error, etc.). Instead, consider using `isObjectType` for a more inclusive validation encompassing any JavaScript "object" type, or `isPlainObject` for a more specific validation targeting simple struct/shape/record-like objects. To replicate the existing logic, use: `const isObject = (x) => isObjectType(x) && !Array.isArray(x)`.** !
+ *
  * @param data the variable to check
  * @signature
  *    R.isObject(data)
@@ -21,6 +25,7 @@ type DefinitelyObject<T> = Exclude<
  *    R.isObject(new Error("error")) //=> true
  *    R.isObject('somethingElse') //=> false
  * @category Guard
+ * @deprecated - The semantics of this guard are confusing. It excludes Arrays but does not exclude other complex objects like Classes or built-in objects (such as Date, Promise, Error, etc.). Instead, consider using `isObjectType` for a more inclusive validation encompassing any JavaScript "object" type, or `isPlainObject` for a more specific validation targeting simple struct/shape/record-like objects. To replicate the existing logic, use: `const isObject = (x) => isObjectType(x) && !Array.isArray(x)`.
  */
 export function isObject<T>(data: T | object): data is DefinitelyObject<T> {
   return !!data && !Array.isArray(data) && typeof data === 'object';
diff --git a/src/isObjectType.ts b/src/isObjectType.ts
new file mode 100644
index 0000000..1319195
--- /dev/null
+++ b/src/isObjectType.ts
@@ -0,0 +1,30 @@
+import type { NarrowedTo } from './_types';
+
+/**
+ * Checks if the given parameter is of type `"object"` via `typeof`, excluding `null`.
+ *
+ * It's important to note that in JavaScript, many entities are considered objects, like Arrays, Classes, RegExps, Maps, Sets, Dates, URLs, Promise, Errors, and more. Although technically an object too, `null` is not considered an object by this function, so that its easier to narrow nullables.
+ *
+ * For a more specific check that is limited to plain objects (simple struct/shape/record-like objects), consider using `isPlainObject` instead. For a simpler check that only removes `null` from the type prefer `isNonNull` or `isDefined`.
+ *
+ * @param data - The variable to be checked for being an object type.
+ * @signature
+ *    R.isObjectType(data)
+ * @returns - The input type, narrowed to only objects.
+ * @example
+ *    // true
+ *    R.isObjectType({}) //=> true
+ *    R.isObjectType([]) //=> true
+ *    R.isObjectType(Promise.resolve("something")) //=> true
+ *    R.isObjectType(new Date()) //=> true
+ *    R.isObjectType(new Error("error")) //=> true
+ *
+ *    // false
+ *    R.isObjectType('somethingElse') //=> false
+ *    R.isObjectType(null) //=> false
+ * @category Guard
+ * @dataFirst
+ */
+export const isObjectType = <T>(
+  data: T | object
+): data is NarrowedTo<T, object> => typeof data === 'object' && data !== null;
diff --git a/src/isPlainObject.ts b/src/isPlainObject.ts
new file mode 100644
index 0000000..122369f
--- /dev/null
+++ b/src/isPlainObject.ts
@@ -0,0 +1,37 @@
+import type { NarrowedTo } from './_types';
+
+/**
+ * Checks if `data` is a "plain" object. A plain object is defined as an object with string keys and values of any type, including primitives, other objects, functions, classes, etc (aka struct/shape/record/simple). Technically, a plain object is one whose prototype is either `Object.prototype` or `null`, ensuring it does not inherit properties or methods from other object types.
+ *
+ * This function is narrower in scope than `isObjectType`, which accepts any entity considered an `"object"` by JavaScript's `typeof`.
+ *
+ * Note that Maps, Arrays, and Sets are not considered plain objects and would return `false`.
+ *
+ * @param data - The variable to check.
+ * @returns - The input type, narrowed to only plain objects.
+ * @signature
+ *    R.isPlainObject(data)
+ * @example
+ *    // true
+ *    R.isPlainObject({}) //=> true
+ *    R.isPlainObject({ a: 123 }) //=> true
+ *
+ *    // false
+ *    R.isPlainObject([]) //=> false
+ *    R.isPlainObject(Promise.resolve("something")) //=> false
+ *    R.isPlainObject(new Date()) //=> false
+ *    R.isPlainObject(new Error("error")) //=> false
+ *    R.isPlainObject('somethingElse') //=> false
+ *    R.isPlainObject(null) //=> false
+ * @category Guard
+ */
+export function isPlainObject<T>(
+  data: T | Record<PropertyKey, unknown>
+): data is NarrowedTo<T, Record<PropertyKey, unknown>> {
+  if (typeof data !== 'object' || data === null) {
+    return false;
+  }
+
+  const proto = Object.getPrototypeOf(data);
+  return proto === null || proto === Object.prototype;
+}


Test patch:
diff --git a/src/isDefined.test.ts b/src/isDefined.test.ts
index 17b76f8..29115fc 100644
--- a/src/isDefined.test.ts
+++ b/src/isDefined.test.ts
@@ -1,5 +1,5 @@
+import { typesDataProvider, type TestClass } from '../test/types_data_provider';
 import { isDefined } from './isDefined';
-import { typesDataProvider } from '../test/types_data_provider';
 
 describe('isDefined', () => {
   test('isDefined": should work as type guard', () => {
@@ -7,16 +7,22 @@ describe('isDefined', () => {
     if (isDefined(data)) {
       expect(data instanceof Date).toEqual(true);
       assertType<
-        | boolean
-        | string
-        | { a: string }
         | (() => void)
+        | [number, number, number]
+        | { a: string }
         | Array<number>
+        | boolean
         | Date
         | Error
+        | Map<string, string>
         | number
         | Promise<number>
+        | RegExp
+        | Set<string>
+        | string
         | symbol
+        | TestClass
+        | Uint8Array
       >(data);
     }
   });
@@ -31,18 +37,22 @@ describe('isDefined', () => {
     expect(data).toHaveLength(4);
     assertType<
       Array<
-        | string
-        | number
-        | boolean
-        | {
-            a: string;
-          }
         | (() => void)
+        | [number, number, number]
+        | { a: string }
         | Array<number>
+        | boolean
         | Date
         | Error
+        | Map<string, string>
+        | number
         | Promise<number>
+        | RegExp
+        | Set<string>
+        | string
         | symbol
+        | TestClass
+        | Uint8Array
       >
     >(data);
   });
@@ -54,17 +64,23 @@ describe('strict', () => {
     if (isDefined.strict(data)) {
       expect(data instanceof Date).toEqual(true);
       assertType<
-        | boolean
-        | string
-        | { a: string }
         | (() => void)
+        | [number, number, number]
+        | { a: string }
         | Array<number>
+        | boolean
         | Date
         | Error
+        | Map<string, string>
+        | null
         | number
         | Promise<number>
-        | null
+        | RegExp
+        | Set<string>
+        | string
         | symbol
+        | TestClass
+        | Uint8Array
       >(data);
     }
   });
@@ -80,19 +96,23 @@ describe('strict', () => {
     expect(data).toHaveLength(5);
     assertType<
       Array<
-        | string
-        | number
-        | boolean
-        | {
-            a: string;
-          }
         | (() => void)
+        | [number, number, number]
+        | { a: string }
         | Array<number>
+        | boolean
         | Date
         | Error
-        | Promise<number>
+        | Map<string, string>
         | null
+        | number
+        | Promise<number>
+        | RegExp
+        | Set<string>
+        | string
         | symbol
+        | TestClass
+        | Uint8Array
       >
     >(data);
   });
diff --git a/src/isNonNull.test.ts b/src/isNonNull.test.ts
index 923b231..ed1cd4d 100644
--- a/src/isNonNull.test.ts
+++ b/src/isNonNull.test.ts
@@ -1,4 +1,4 @@
-import { typesDataProvider } from '../test/types_data_provider';
+import { typesDataProvider, type TestClass } from '../test/types_data_provider';
 import { isNonNull } from './isNonNull';
 
 describe('isNonNull', () => {
@@ -7,17 +7,23 @@ describe('isNonNull', () => {
     if (isNonNull(data)) {
       expect(data instanceof Date).toEqual(true);
       assertType<
-        | boolean
-        | string
-        | { a: string }
         | (() => void)
+        | [number, number, number]
+        | { a: string }
         | Array<number>
+        | boolean
         | Date
         | Error
+        | Map<string, string>
         | number
         | Promise<number>
-        | undefined
+        | RegExp
+        | Set<string>
+        | string
         | symbol
+        | TestClass
+        | Uint8Array
+        | undefined
       >(data);
     }
   });
@@ -33,19 +39,23 @@ describe('isNonNull', () => {
     expect(data).toHaveLength(5);
     assertType<
       Array<
-        | string
-        | number
-        | boolean
-        | {
-            a: string;
-          }
         | (() => void)
+        | [number, number, number]
+        | { a: string }
         | Array<number>
+        | boolean
         | Date
         | Error
+        | Map<string, string>
+        | number
         | Promise<number>
-        | undefined
+        | RegExp
+        | Set<string>
+        | string
         | symbol
+        | TestClass
+        | Uint8Array
+        | undefined
       >
     >(data);
   });
diff --git a/src/isNot.test.ts b/src/isNot.test.ts
index 87e9bb0..120df99 100644
--- a/src/isNot.test.ts
+++ b/src/isNot.test.ts
@@ -1,7 +1,7 @@
+import { typesDataProvider, type TestClass } from '../test/types_data_provider';
 import { isNot } from './isNot';
 import { isPromise } from './isPromise';
 import { isString } from './isString';
-import { typesDataProvider } from '../test/types_data_provider';
 
 describe('isNot', () => {
   test('isNot: should work as type guard', () => {
@@ -9,18 +9,22 @@ describe('isNot', () => {
     if (isNot(isString)(data)) {
       expect(data instanceof Promise).toEqual(true);
       assertType<
-        | number
-        | boolean
-        | {
-            a: string;
-          }
         | (() => void)
+        | [number, number, number]
+        | { a: string }
         | Array<number>
+        | boolean
         | Date
         | Error
+        | Map<string, string>
+        | null
+        | number
         | Promise<number>
+        | RegExp
+        | Set<string>
         | symbol
-        | null
+        | TestClass
+        | Uint8Array
         | undefined
       >(data);
     }
@@ -37,17 +41,23 @@ describe('isNot', () => {
 
     assertType<
       Array<
-        | boolean
-        | string
-        | { a: string }
         | (() => void)
+        | [number, number, number]
+        | { a: string }
         | Array<number>
+        | boolean
         | Date
-        | undefined
-        | null
         | Error
+        | Map<string, string>
+        | null
         | number
+        | RegExp
+        | Set<string>
+        | string
         | symbol
+        | TestClass
+        | Uint8Array
+        | undefined
       >
     >(result);
   });
diff --git a/src/isObject.test.ts b/src/isObject.test.ts
index f81099d..40444ec 100644
--- a/src/isObject.test.ts
+++ b/src/isObject.test.ts
@@ -1,4 +1,8 @@
-import { typesDataProvider } from '../test/types_data_provider';
+import {
+  ALL_TYPES_DATA_PROVIDER,
+  typesDataProvider,
+  type TestClass,
+} from '../test/types_data_provider';
 import { isObject } from './isObject';
 
 describe('isObject', () => {
@@ -7,12 +11,15 @@ describe('isObject', () => {
     if (isObject(data)) {
       expect(typeof data).toEqual('object');
       assertType<
-        | {
-            a: string;
-          }
+        | { a: string }
         | Date
         | Error
+        | Map<string, string>
         | Promise<number>
+        | RegExp
+        | Set<string>
+        | TestClass
+        | Uint8Array
       >(data);
     }
   });
@@ -46,24 +53,21 @@ describe('isObject', () => {
   });
 
   test('isObject: should work as type guard in filter', () => {
-    const data = [
-      typesDataProvider('promise'),
-      typesDataProvider('array'),
-      typesDataProvider('boolean'),
-      typesDataProvider('function'),
-      typesDataProvider('object'),
-    ].filter(isObject);
+    const data = ALL_TYPES_DATA_PROVIDER.filter(isObject);
     expect(data.every(c => typeof c === 'object' && !Array.isArray(c))).toEqual(
       true
     );
     assertType<
       Array<
-        | {
-            a: string;
-          }
+        | { a: string }
         | Date
         | Error
+        | Map<string, string>
         | Promise<number>
+        | RegExp
+        | Set<string>
+        | TestClass
+        | Uint8Array
       >
     >(data);
   });
diff --git a/src/isObjectType.test.ts b/src/isObjectType.test.ts
new file mode 100644
index 0000000..b07b015
--- /dev/null
+++ b/src/isObjectType.test.ts
@@ -0,0 +1,122 @@
+import {
+  ALL_TYPES_DATA_PROVIDER,
+  TestClass,
+  typesDataProvider,
+} from '../test/types_data_provider';
+import { isObjectType } from './isObjectType';
+
+describe('runtime', () => {
+  it('accepts simple objects', () => {
+    expect(isObjectType({ a: 123 })).toEqual(true);
+  });
+
+  it('accepts trivial empty objects', () => {
+    expect(isObjectType({})).toEqual(true);
+  });
+
+  it('rejects strings', () => {
+    expect(isObjectType('asd')).toEqual(false);
+  });
+
+  it('rejects null', () => {
+    expect(isObjectType(null)).toEqual(false);
+  });
+
+  it('accepts arrays', () => {
+    expect(isObjectType([1, 2, 3])).toEqual(true);
+  });
+
+  it('accepts classes', () => {
+    expect(isObjectType(new TestClass())).toEqual(true);
+  });
+
+  it('accepts null prototypes', () => {
+    expect(isObjectType(Object.create(null))).toEqual(true);
+  });
+
+  test('ALL_TYPES_DATA_PROVIDER', () => {
+    expect(ALL_TYPES_DATA_PROVIDER.filter(isObjectType)).toMatchInlineSnapshot(`
+      [
+        [
+          1,
+          2,
+          3,
+        ],
+        1985-07-24T07:40:00.000Z,
+        [Error: asd],
+        TestClass {},
+        Map {},
+        {
+          "a": "asd",
+        },
+        Promise {},
+        /test/gu,
+        Set {},
+        [
+          1,
+          2,
+          3,
+        ],
+        Uint8Array [
+          0,
+        ],
+      ]
+    `);
+  });
+});
+
+describe('typing', () => {
+  test('narrows nullable types', () => {
+    const data: { a: string } | null = { a: 'hello' };
+    if (isObjectType(data)) {
+      expectTypeOf(data).toEqualTypeOf<{ a: string }>();
+    }
+  });
+
+  test('isObjectType: should work as type guard', () => {
+    const data = typesDataProvider('object');
+    if (isObjectType(data)) {
+      expectTypeOf(data).toEqualTypeOf<
+        | (() => void)
+        | [number, number, number]
+        | { a: string }
+        | Array<number>
+        | Date
+        | Error
+        | Map<string, string>
+        | Promise<number>
+        | RegExp
+        | Set<string>
+        | TestClass
+        | Uint8Array
+      >();
+    }
+  });
+
+  test('should work even if data type is unknown', () => {
+    const data: unknown = typesDataProvider('object');
+    if (isObjectType(data)) {
+      expectTypeOf(data).toEqualTypeOf<object>();
+    }
+  });
+
+  test('isObjectType: should work as type guard in filter', () => {
+    const data = ALL_TYPES_DATA_PROVIDER.filter(isObjectType);
+    expectTypeOf(data).toEqualTypeOf<
+      Array<
+        | RegExp
+        | (() => void)
+        | [number, number, number]
+        | { a: string }
+        | Array<number>
+        | Date
+        | Error
+        | Promise<number>
+        | TestClass
+        | Set<string>
+        | Map<string, string>
+        | Uint8Array
+      >
+    >();
+  });
+});
diff --git a/src/isPlainObject.test.ts b/src/isPlainObject.test.ts
new file mode 100644
index 0000000..6058711
--- /dev/null
+++ b/src/isPlainObject.test.ts
@@ -0,0 +1,78 @@
+import {
+  ALL_TYPES_DATA_PROVIDER,
+  TestClass,
+  typesDataProvider,
+} from '../test/types_data_provider';
+import { isPlainObject } from './isPlainObject';
+
+describe('runtime', () => {
+  it('accepts simple objects', () => {
+    expect(isPlainObject({ a: 123 })).toEqual(true);
+  });
+
+  it('accepts trivial empty objects', () => {
+    expect(isPlainObject({})).toEqual(true);
+  });
+
+  it('rejects strings', () => {
+    expect(isPlainObject('asd')).toEqual(false);
+  });
+
+  it('rejects arrays', () => {
+    expect(isPlainObject([1, 2, 3])).toEqual(false);
+  });
+
+  it('rejects classes', () => {
+    expect(isPlainObject(new TestClass())).toEqual(false);
+  });
+
+  it('accepts null prototypes', () => {
+    expect(isPlainObject(Object.create(null))).toEqual(true);
+  });
+
+  test('ALL_TYPES_DATA_PROVIDER', () => {
+    expect(ALL_TYPES_DATA_PROVIDER.filter(isPlainObject))
+      .toMatchInlineSnapshot(`
+        [
+          {
+            "a": "asd",
+          },
+        ]
+      `);
+  });
+});
+
+describe('typing', () => {
+  test('narrows readonly records', () => {
+    const data: { readonly a: 123 } = { a: 123 };
+    if (isPlainObject(data)) {
+      expectTypeOf(data).toEqualTypeOf<{ readonly a: 123 }>();
+    }
+  });
+
+  test('narrows mixed records', () => {
+    const data: { readonly a: 123; b: boolean } = { a: 123, b: false };
+    if (isPlainObject(data)) {
+      expectTypeOf(data).toEqualTypeOf<{ readonly a: 123; b: boolean }>();
+    }
+  });
+
+  test('should work as type guard', () => {
+    const data = typesDataProvider('object');
+    if (isPlainObject(data)) {
+      expectTypeOf(data).toEqualTypeOf<{ a: string }>();
+    }
+  });
+
+  test('should work even if data type is unknown', () => {
+    const data: unknown = typesDataProvider('object');
+    if (isPlainObject(data)) {
+      expectTypeOf(data).toEqualTypeOf<Record<PropertyKey, unknown>>();
+    }
+  });
+
+  test('should work as type guard in filter', () => {
+    const data = ALL_TYPES_DATA_PROVIDER.filter(isPlainObject);
+    expectTypeOf(data).toEqualTypeOf<Array<{ a: string }>>();
+  });
+});
diff --git a/test/types_data_provider.ts b/test/types_data_provider.ts
index 527b1c5..ba90032 100644
--- a/test/types_data_provider.ts
+++ b/test/types_data_provider.ts
@@ -1,41 +1,62 @@
+export class TestClass {
+  get foo() {
+    return 'a';
+  }
+}
+
 type TestObj =
-  | boolean
-  | string
-  | { a: string }
   | (() => void)
+  | [number, number, number]
+  | { a: string }
   | Array<number>
+  | boolean
   | Date
-  | undefined
-  | null
   | Error
+  | Map<string, string>
+  | null
   | number
   | Promise<number>
-  | symbol;
+  | RegExp
+  | Set<string>
+  | string
+  | symbol
+  | TestClass
+  | Uint8Array
+  | undefined;
+
+const ALL_TYPES = [
+  'array',
+  'boolean',
+  'date',
+  'error',
+  'function',
+  'instance',
+  'map',
+  'null',
+  'number',
+  'object',
+  'promise',
+  'regex',
+  'set',
+  'string',
+  'symbol',
+  'tuple',
+  'typedArray',
+  'undefined',
+] as const;
+type Type = (typeof ALL_TYPES)[number];
 
-export const typesDataProvider = (
-  t:
-    | 'string'
-    | 'boolean'
-    | 'object'
-    | 'function'
-    | 'array'
-    | 'date'
-    | 'undefined'
-    | 'null'
-    | 'error'
-    | 'number'
-    | 'promise'
-    | 'symbol'
-): TestObj => {
+export const typesDataProvider = (t: Type): TestObj => {
   switch (t) {
     case 'number':
       return 5;
     case 'array':
+    case 'tuple':
       return [1, 2, 3];
     case 'boolean':
       return false;
     case 'date':
-      return new Date();
+      return new Date('1985-07-24T07:40:00.000Z');
     case 'function':
       return () => {
         /* (intentionally empty) */
@@ -54,5 +75,17 @@ export const typesDataProvider = (
       return Symbol('symbol');
     case 'undefined':
       return undefined;
+    case 'instance':
+      return new TestClass();
+    case 'regex':
+      return /test/gu;
+    case 'map':
+      return new Map();
+    case 'set':
+      return new Set();
+    case 'typedArray':
+      return new Uint8Array(1);
   }
 };
+
+export const ALL_TYPES_DATA_PROVIDER = ALL_TYPES.map(typesDataProvider);