Repository: blitz-js/superjson (ts)

Issue:
Serialize Error?
Hi folks! I thought I'll drop this small feature request.

The dreamlike thing I'd like to do is deserializing backend error and throwing it to a React ErrorBoundary.

Currently I get this:

> SerializableError: Error serializing `.json.error` returned from `getServerSideProps` in "/meetings".
> Reason: `object` ("[object Error]") cannot be serialized as JSON. Please only return JSON serializable data types.


Pull request:
Add support for serialising Errors
SuperJSON now natively supports serialising the `Error` type.

Closes #74 

Patch:
diff --git a/README.md b/README.md
index fc24fee..4da838b 100644
--- a/README.md
+++ b/README.md
@@ -202,18 +202,19 @@ Superjson supports many extra types which JSON does not. You can serialize all t
 
 | type        | supported by standard JSON? | supported by Superjson? |
 | ----------- | --------------------------- | ----------------------- |
-| `string`    | ✅                          | ✅                      |
-| `number`    | ✅                          | ✅                      |
-| `boolean`   | ✅                          | ✅                      |
-| `null`      | ✅                          | ✅                      |
-| `Array`     | ✅                          | ✅                      |
-| `Object`    | ✅                          | ✅                      |
-| `undefined` | ❌                          | ✅                      |
-| `bigint`    | ❌                          | ✅                      |
-| `Date`      | ❌                          | ✅                      |
-| `RegExp`    | ❌                          | ✅                      |
-| `Set`       | ❌                          | ✅                      |
-| `Map`       | ❌                          | ✅                      |
+| `string`    | ✅                           | ✅                       |
+| `number`    | ✅                           | ✅                       |
+| `boolean`   | ✅                           | ✅                       |
+| `null`      | ✅                           | ✅                       |
+| `Array`     | ✅                           | ✅                       |
+| `Object`    | ✅                           | ✅                       |
+| `undefined` | ❌                           | ✅                       |
+| `bigint`    | ❌                           | ✅                       |
+| `Date`      | ❌                           | ✅                       |
+| `RegExp`    | ❌                           | ✅                       |
+| `Set`       | ❌                           | ✅                       |
+| `Map`       | ❌                           | ✅                       |
+| `Error`     | ❌                           | ✅                       |
 
 ## Contributors ✨
 
diff --git a/src/is.ts b/src/is.ts
index 74e6a4d..9fceb9f 100644
--- a/src/is.ts
+++ b/src/is.ts
@@ -46,6 +46,9 @@ export const isSymbol = (payload: any): payload is symbol =>
 export const isDate = (payload: any): payload is Date =>
   payload instanceof Date && !isNaN(payload.valueOf());
 
+export const isError = (payload: any): payload is Error =>
+  payload instanceof Error;
+
 export const isNaNValue = (payload: any): payload is typeof NaN =>
   typeof payload === 'number' && isNaN(payload);
 
diff --git a/src/transformer.ts b/src/transformer.ts
index 31c5dbc..54dd8c3 100644
--- a/src/transformer.ts
+++ b/src/transformer.ts
@@ -12,12 +12,12 @@ import {
 } from './is';
 import { ClassRegistry } from './class-registry';
 import { SymbolRegistry } from './symbol-registry';
-import { fromPairs, includes, entries, find } from 'lodash';
+import { fromPairs, includes, entries, find, isError } from 'lodash';
 import { CustomTransformerRegistry } from './custom-transformer-registry';
 
 export type PrimitiveTypeAnnotation = 'number' | 'undefined' | 'bigint';
 
-type LeafTypeAnnotation = PrimitiveTypeAnnotation | 'regexp' | 'Date';
+type LeafTypeAnnotation = PrimitiveTypeAnnotation | 'regexp' | 'Date' | 'Error';
 
 type ClassTypeAnnotation = ['class', string];
 type SymbolTypeAnnotation = ['symbol', string];
@@ -45,7 +45,7 @@ export const isPrimitiveTypeAnnotation = (
 };
 
 const ALL_TYPE_ANNOTATIONS: TypeAnnotation[] = ALL_PRIMITIVE_TYPE_ANNOTATIONS.concat(
-  ['map', 'regexp', 'set', 'Date']
+  ['map', 'regexp', 'set', 'Date', 'Error']
 );
 
 export const isTypeAnnotation = (value: any): value is TypeAnnotation => {
@@ -98,6 +98,18 @@ const simpleRules = [
     v => new Date(v)
   ),
 
+  simpleTransformation(
+    isError,
+    'Error',
+    v => ({ name: v.name, message: v.message, stack: v.stack }),
+    ({ name, message, stack }) => {
+      const e = new Error(message);
+      e.name = name;
+      e.stack = stack;
+      return e;
+    }
+  ),
+
   simpleTransformation(
     isRegExp,
     'regexp',


Test patch:
diff --git a/src/index.test.ts b/src/index.test.ts
index d09a5f7..25fd2e2 100644
--- a/src/index.test.ts
+++ b/src/index.test.ts
@@ -13,7 +13,7 @@ describe('stringify & parse', () => {
     string,
     {
       input: (() => SuperJSONValue) | SuperJSONValue;
-      output: JSONValue;
+      output: JSONValue | ((v: JSONValue) => void);
       outputAnnotations?: Annotations;
       customExpectations?: (value: any) => void;
     }
@@ -181,6 +181,24 @@ describe('stringify & parse', () => {
       },
     },
 
+    'works for Errors': {
+      input: {
+        e: new Error('epic fail'),
+      },
+      output: ({ e }: any) => {
+        expect(e.name).toBe('Error');
+        expect(e.message).toBe('epic fail');
+        expect(e.stack.startsWith('Error: epic fail\n    at Suite.')).toBe(
+          true
+        );
+      },
+      outputAnnotations: {
+        values: {
+          e: ['Error'],
+        },
+      },
+    },
+
     'works for regex': {
       input: {
         a: /hello/g,
@@ -552,7 +570,11 @@ describe('stringify & parse', () => {
       deepFreeze(inputValue);
       const { json, meta } = SuperJSON.serialize(inputValue);
 
-      expect(json).toEqual(expectedOutput);
+      if (typeof expectedOutput === 'function') {
+        expectedOutput(json);
+      } else {
+        expect(json).toEqual(expectedOutput);
+      }
       expect(meta).toEqual(expectedOutputAnnotations);
 
       const untransformed = SuperJSON.deserialize({ json, meta });