Repository: blitz-js/superjson
Pull request #13 (2020-07-22 11:26:05)

Implement custom type checking
It turns out that `@sindresorhus/is` is actually quite big! Right now it accounts for 64% of our [bundle size](https://bundlephobia.com/result?p=superjson@1.0.1). This is because it provides much more functionality than we actually need–we only use a handful of `is.whatever` functions. I’ve therefore written a custom type-checking util which should be much more lightweight. This also means we can brag about having zero external dependencies 😄.

flybayer: Unused stuff should tree-shake out if you do this 🤔 

```ts
import {map as isMap} from '@sindresorhus/is
```
merelinguist: @flybayer No expert here but I think the problem is that it doesn’t have any named exports–just one big `is` function. So we can’t import functions individually like that.
Skn0tt: Good catch on the bundle size! I'd like to have a look into wether it's really impossible to tree-shake `@sindresorhus/is` though - it looks like it should be. The Bundlephobia link says `This package exports ES6 modules, but isn't marked side-effect free.` on the bottom, which makes me think it's a problem on our side 🙈

On "brag about having zero external dependencies": I see where you're coming from, but this is a very generic problem which I don't feel we should reinvent. Personally, I'd prefer using smth battle-proof instead of rolling our own here.
merelinguist: @Skn0tt If you can find a way to fix the bundle size using `@sindresorhus/is` then I’m happy to continue using it. :) Either way, as indicated by brevity of the code I’ve added, this is a relatively simple problem, so it definitely shouldn’t be such a large part of our bundle size.
Skn0tt: Will have a look into the tree shaking part.

When I saw `Object.prototype.toString.call`, I guess I was kinda scared of users breaking this by doing wonky stuff with prototype chains etc. So I guess I'm not really concerned about bundle size, but rather about maintaining it - I don't know anything about the whole prototype chain stuff ...
Skn0tt: Alright, I'm dumb. `@sindresorhus/is` [does exactly the same...](https://github.com/sindresorhus/is/blob/e31db97eab467f2c180c857dd7474accf8ac2215/source/index.ts#L87).

Patch:
diff --git a/package.json b/package.json
index 147d737..2266046 100644
--- a/package.json
+++ b/package.json
@@ -47,12 +47,10 @@
   "devDependencies": {
     "husky": "^4.2.5",
     "import-sort-style-module": "^6.0.0",
+    "prettier": "^2.0.5",
     "prettier-plugin-import-sort": "^0.0.4",
     "tsdx": "^0.13.2",
     "tslib": "^2.0.0",
     "typescript": "^3.9.6"
-  },
-  "dependencies": {
-    "@sindresorhus/is": "^3.0.0"
   }
 }
diff --git a/src/annotator.ts b/src/annotator.ts
index ee35f31..00c557c 100644
--- a/src/annotator.ts
+++ b/src/annotator.ts
@@ -1,5 +1,4 @@
-import is from '@sindresorhus/is';
-
+import { isMap } from './is';
 import { mapDeep } from './mapDeep';
 import {
   StringifiedPath,
@@ -49,7 +48,7 @@ export const makeAnnotator = () => {
   const annotations: Annotations = {};
 
   const annotator: Walker = ({ path, node }) => {
-    if (is.map(node)) {
+    if (isMap(node)) {
       const newNode = new Map<string, any>();
 
       for (const [key, value] of node.entries()) {
@@ -102,7 +101,7 @@ export const applyAnnotations = (plain: any, annotations: Annotations): any => {
     );
 
     for (const [path, type] of annotationsWithPathsLeavesToRoot) {
-      plain = mapDeep(plain, path, v =>
+      plain = mapDeep(plain, path, (v) =>
         untransformValue(v, type as TypeAnnotation)
       );
     }
diff --git a/src/index.ts b/src/index.ts
index db4a4d9..d495e39 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,6 +1,5 @@
-import is from '@sindresorhus/is';
-
 import { applyAnnotations, makeAnnotator } from './annotator';
+import { isEmptyObject } from './is';
 import { plainer } from './plainer';
 import { SuperJSONResult, SuperJSONValue, isSuperJSONResult } from './types';
 
@@ -10,7 +9,7 @@ export const serialize = (object: SuperJSONValue): SuperJSONResult => {
 
   return {
     json: output,
-    meta: is.emptyObject(annotations) ? undefined : annotations,
+    meta: isEmptyObject(annotations) ? undefined : annotations,
   };
 };
 
diff --git a/src/is.ts b/src/is.ts
new file mode 100644
index 0000000..3b95495
--- /dev/null
+++ b/src/is.ts
@@ -0,0 +1,67 @@
+const getType = (payload: any): string =>
+  Object.prototype.toString.call(payload).slice(8, -1);
+
+export const isUndefined = (payload: any): payload is undefined =>
+  getType(payload) === 'Undefined';
+
+export const isNull = (payload: any): payload is null =>
+  getType(payload) === 'Null';
+
+export const isPlainObject = (
+  payload: any
+): payload is { [key: string]: any } => {
+  if (getType(payload) !== 'Object') return false;
+  return (
+    payload.constructor === Object &&
+    Object.getPrototypeOf(payload) === Object.prototype
+  );
+};
+
+export const isEmptyObject = (payload: any): payload is {} =>
+  isPlainObject(payload) && Object.keys(payload).length === 0;
+
+export const isArray = (payload: any): payload is any[] =>
+  getType(payload) === 'Array';
+
+export const isString = (payload: any): payload is string =>
+  getType(payload) === 'String';
+
+export const isNumber = (payload: any): payload is number =>
+  getType(payload) === 'Number' && !isNaN(payload);
+
+export const isBoolean = (payload: any): payload is boolean =>
+  getType(payload) === 'Boolean';
+
+export const isRegExp = (payload: any): payload is RegExp =>
+  getType(payload) === 'RegExp';
+
+export const isMap = (payload: any): payload is Map<any, any> =>
+  getType(payload) === 'Map';
+
+export const isSet = (payload: any): payload is Set<any> =>
+  getType(payload) === 'Set';
+
+export const isSymbol = (payload: any): payload is symbol =>
+  getType(payload) === 'Symbol';
+
+export const isDate = (payload: any): payload is Date =>
+  getType(payload) === 'Date' && !isNaN(payload);
+
+export const isNaNValue = (payload: any): payload is typeof NaN =>
+  getType(payload) === 'Number' && isNaN(payload);
+
+export const isPrimitive = (
+  payload: any
+): payload is boolean | null | undefined | number | string | symbol =>
+  isBoolean(payload) ||
+  isNull(payload) ||
+  isUndefined(payload) ||
+  isNumber(payload) ||
+  isString(payload) ||
+  isSymbol(payload);
+
+export const isBigint = (payload: any): payload is bigint =>
+  getType(payload) === 'BigInt';
+
+export const isInfinite = (payload: any): payload is number =>
+  payload === Infinity || payload === -Infinity;
diff --git a/src/mapDeep.ts b/src/mapDeep.ts
index 4ef7479..7eb94d5 100644
--- a/src/mapDeep.ts
+++ b/src/mapDeep.ts
@@ -1,4 +1,4 @@
-import is from '@sindresorhus/is';
+import { isArray } from './is';
 
 export const mapDeep = (
   object: object,
@@ -11,7 +11,7 @@ export const mapDeep = (
 
   const [head, ...tail] = path;
 
-  if (is.array(object)) {
+  if (isArray(object)) {
     return object.map((v, i) => {
       if (i === head) {
         return mapDeep(v, tail, mapper);
diff --git a/src/pathstringifier.ts b/src/pathstringifier.ts
index 7b5dfe5..6d0eebf 100644
--- a/src/pathstringifier.ts
+++ b/src/pathstringifier.ts
@@ -5,10 +5,7 @@ const escape = (key: string) => key.replace(/\./g, '\\.');
 const unescape = (k: string) => k.replace(/\\\./g, '.');
 
 export const stringifyPath = (path: Path): StringifiedPath =>
-  path
-    .map(String)
-    .map(escape)
-    .join('.');
+  path.map(String).map(escape).join('.');
 
 export const parsePath = (string: StringifiedPath): Path =>
   string.split(/(?<!\\)\./g).map(unescape);
diff --git a/src/plainer.ts b/src/plainer.ts
index cf69798..28e4343 100644
--- a/src/plainer.ts
+++ b/src/plainer.ts
@@ -1,4 +1,4 @@
-import is from '@sindresorhus/is';
+import { isArray, isMap, isPlainObject, isPrimitive, isSet } from './is';
 
 interface WalkerValue {
   isLeaf: boolean;
@@ -9,17 +9,14 @@ interface WalkerValue {
 export type Walker = (v: WalkerValue) => any;
 
 const isDeep = (object: any): boolean =>
-  is.plainObject(object) ||
-  is.array(object) ||
-  is.map(object) ||
-  is.set(object);
+  isPlainObject(object) || isArray(object) || isMap(object) || isSet(object);
 
 const entries = (object: object | Map<any, any>): [any, any][] => {
-  if (is.map(object)) {
+  if (isMap(object)) {
     return [...object.entries()];
   }
 
-  if (is.plainObject(object)) {
+  if (isPlainObject(object)) {
     return Object.entries(object);
   }
 
@@ -40,19 +37,19 @@ export const plainer = (
     throw new TypeError('Circular Reference');
   }
 
-  if (!is.primitive(object)) {
+  if (!isPrimitive(object)) {
     alreadySeenObjects.add(object);
   }
 
   walker({ isLeaf: false, path, node: object });
 
-  if (is.array(object) || is.set(object)) {
+  if (isArray(object) || isSet(object)) {
     return [...object].map((value, key) =>
       plainer(value, walker, [...path, key], alreadySeenObjects)
     );
   }
 
-  if (is.plainObject(object) || is.map(object)) {
+  if (isPlainObject(object) || isMap(object)) {
     return Object.fromEntries(
       entries(object).map(([key, value]) => [
         key,
diff --git a/src/transformer.ts b/src/transformer.ts
index cf6fec9..058a215 100644
--- a/src/transformer.ts
+++ b/src/transformer.ts
@@ -1,4 +1,15 @@
-import is from '@sindresorhus/is';
+import {
+  isBigint,
+  isBoolean,
+  isDate,
+  isInfinite,
+  isMap,
+  isNaNValue,
+  isNumber,
+  isRegExp,
+  isSet,
+  isUndefined,
+} from './is';
 
 export type PrimitiveTypeAnnotation =
   | 'NaN'
@@ -38,42 +49,42 @@ export const isTypeAnnotation = (value: any): value is TypeAnnotation => {
 export const transformValue = (
   value: any
 ): { value: any; type: TypeAnnotation } | undefined => {
-  if (is.undefined(value)) {
+  if (isUndefined(value)) {
     return {
       value: undefined,
       type: 'undefined',
     };
-  } else if (is.bigint(value)) {
+  } else if (isBigint(value)) {
     return {
       value: value.toString(),
       type: 'bigint',
     };
-  } else if (is.date(value)) {
+  } else if (isDate(value)) {
     return {
       value: value.toISOString(),
       type: 'Date',
     };
-  } else if (is.nan(value)) {
+  } else if (isNaNValue(value)) {
     return {
       value: undefined,
       type: 'NaN',
     };
-  } else if (is.infinite(value)) {
+  } else if (isInfinite(value)) {
     return {
       value: undefined,
       type: value > 0 ? 'Infinity' : '-Infinity',
     };
-  } else if (is.set(value)) {
+  } else if (isSet(value)) {
     return {
       value: Array.from(value) as any[],
       type: 'set',
     };
-  } else if (is.regExp(value)) {
+  } else if (isRegExp(value)) {
     return {
       value: '' + value,
       type: 'regexp',
     };
-  } else if (is.map(value)) {
+  } else if (isMap(value)) {
     return {
       value: value,
       type: 'map',
@@ -127,11 +138,11 @@ export function isKeyTypeAnnotation(
 export function transformKey(
   key: any
 ): { key: string; type: KeyTypeAnnotation } | undefined {
-  if (is.number(key)) {
+  if (isNumber(key)) {
     return { key: '' + key, type: 'number' };
   }
 
-  if (is.boolean(key)) {
+  if (isBoolean(key)) {
     return { key: '' + key, type: 'boolean' };
   }
 
diff --git a/src/types.ts b/src/types.ts
index e7a4a85..0fb61b0 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,6 +1,5 @@
-import is from '@sindresorhus/is';
-
 import { Annotations, isAnnotations } from './annotator';
+import { isUndefined } from './is';
 
 export type PrimitveJSONValue = string | number | boolean | undefined | null;
 
@@ -45,11 +44,11 @@ export interface SuperJSONResult {
 }
 
 export function isSuperJSONResult(object: any): object is SuperJSONResult {
-  if (is.undefined(object.json)) {
+  if (isUndefined(object.json)) {
     return false;
   }
 
-  if (is.undefined(object.meta)) {
+  if (isUndefined(object.meta)) {
     return true;
   }
 
diff --git a/yarn.lock b/yarn.lock
index a8bab3d..e67ab06 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1092,11 +1092,6 @@
     estree-walker "^1.0.1"
     picomatch "^2.2.2"
 
-"@sindresorhus/is@^3.0.0":
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-3.0.0.tgz#78fabc5e295adb6e1ef57eaafe4cc5d7aa35b183"
-  integrity sha512-kqA5I6Yun7PBHk8WN9BBP1c7FfN2SrD05GuVSEYPqDb4nerv7HqYfgBfMIKmT/EuejURkJKLZuLyGKGs6WEG9w==
-
 "@types/babel__core@^7.1.0":
   version "7.1.9"
   resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.9.tgz#77e59d438522a6fb898fa43dc3455c6e72f3963d"
@@ -4781,6 +4776,11 @@ prettier@^1.19.1:
   resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
   integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
 
+prettier@^2.0.5:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4"
+  integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==
+
 pretty-format@^24.9.0:
   version "24.9.0"
   resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.9.0.tgz#12fac31b37019a4eea3c11aa9a959eb7628aa7c9"


Test patch:
diff --git a/src/is.test.ts b/src/is.test.ts
new file mode 100644
index 0000000..3bb48e9
--- /dev/null
+++ b/src/is.test.ts
@@ -0,0 +1,69 @@
+import {
+  isArray,
+  isBoolean,
+  isDate,
+  isNull,
+  isNumber,
+  isPrimitive,
+  isRegExp,
+  isString,
+  isSymbol,
+  isUndefined,
+} from './is';
+
+test('Basic true tests', () => {
+  expect(isUndefined(undefined)).toBe(true);
+  expect(isNull(null)).toBe(true);
+
+  expect(isArray([])).toBe(true);
+  expect(isArray(new Array())).toBe(true);
+  expect(isString('')).toBe(true);
+  expect(isString('_')).toBe(true);
+
+  expect(isBoolean(true)).toBe(true);
+  expect(isBoolean(false)).toBe(true);
+  expect(isRegExp(/./)).toBe(true);
+  expect(isRegExp(/./gi)).toBe(true);
+  expect(isNumber(0)).toBe(true);
+  expect(isNumber(1)).toBe(true);
+  expect(isDate(new Date())).toBe(true);
+  expect(isSymbol(Symbol())).toBe(true);
+});
+
+test('Basic false tests', () => {
+  expect(isNumber(NaN)).toBe(false);
+  expect(isDate(new Date('_'))).toBe(false);
+  expect(isDate(NaN)).toBe(false);
+  expect(isUndefined(NaN)).toBe(false);
+  expect(isNull(NaN)).toBe(false);
+
+  expect(isArray(NaN)).toBe(false);
+  expect(isString(NaN)).toBe(false);
+
+  expect(isBoolean(NaN)).toBe(false);
+  expect(isRegExp(NaN)).toBe(false);
+  expect(isSymbol(NaN)).toBe(false);
+});
+
+test('Primitive tests', () => {
+  expect(isPrimitive(0)).toBe(true);
+  expect(isPrimitive('')).toBe(true);
+  expect(isPrimitive('str')).toBe(true);
+  expect(isPrimitive(Symbol())).toBe(true);
+  expect(isPrimitive(true)).toBe(true);
+  expect(isPrimitive(false)).toBe(true);
+  expect(isPrimitive(null)).toBe(true);
+  expect(isPrimitive(undefined)).toBe(true);
+
+  expect(isPrimitive(NaN)).toBe(false);
+  expect(isPrimitive([])).toBe(false);
+  expect(isPrimitive(new Array())).toBe(false);
+  expect(isPrimitive({})).toBe(false);
+  expect(isPrimitive(new Object())).toBe(false);
+  expect(isPrimitive(new Date())).toBe(false);
+  expect(isPrimitive(() => {})).toBe(false);
+});
+
+test('Date exception', () => {
+  expect(isDate(new Date('_'))).toBe(false);
+});