Repository: jaredpalmer/formik (ts)

Issue:
Formik removes class prototype on top level state
## 🐛 Bug report

### Current Behavior

If you use a class for the initialValues prop, when you update anything on that top level, Formik converts it to a basic Javascript object. It does not do this for nested objects; just the top level.

### Expected behavior

Formik should keep the class methods and prototype when the form updates at top level.

### Reproducible example

https://codesandbox.io/s/8nkm0nrz59
Note that changing any box will cause the top level object to no longer be an instanceof FormikState, but changing the third box does not cause the nested object to lose its instanceof FormikNestedState

### Suggested solution(s)

Haven't looked into the code yet, but I'm assuming there's something like an `Object.assign({}, updatedState)` or similar. Instead, it should be something like `Object.assign(oldState, updates)`.
Edit: Looks like this is the issue: https://github.com/jaredpalmer/formik/blob/3242489e0cd7b68bad16494b749e43129a2cadbd/src/utils.ts#L57

### Additional context

We're trying to use a method on the initialState class object to determine if we should disable a button or not. Having the class get blown away makes that not possible from within that class. We have that logic in the component itself, but would like to abstract parts of it away into this class. The fact that this behavior is inconsistent at top level vs nested means this is probably an edge case.

### Your environment

| Software         | Version(s) |
| ---------------- | ---------- |
| Formik           | 1.5.1 (latest atm)
| React            | 16.8.3
| TypeScript       | 3.3.3333
| Browser          | Chrome
| npm/Yarn         | yarn 1.13.0
| Operating System | macOs


Pull request:
setIn keeps class inheritance for the top level object
setIn is causing the class inheritance of the top level object to get broken, set back to a regular Javascript object. This fixes it by cloning the original object and manipulating it instead of spreading the object and changes into a new Javascript object at the end.

This will resolve #1369

Patch:
diff --git a/.storybook/webpack.config.js b/.storybook/webpack.config.js
index 8e8ff78..a217a65 100644
--- a/.storybook/webpack.config.js
+++ b/.storybook/webpack.config.js
@@ -6,8 +6,10 @@ module.exports = (storybookBaseConfig, configType, defaultConfig) => {
   defaultConfig.module.rules.push(
     {
       test: /\.tsx?$/,
-      exclude: /node_modules/,
       loader: 'awesome-typescript-loader',
+      options: {
+        configFileName: 'storybook.tsconfig.json'
+      }
     },
   )
   defaultConfig.resolve.extensions = ['.ts', '.tsx', '.js', '.jsx']
diff --git a/src/utils.ts b/src/utils.ts
index 8c7bb10..b09c3d7 100755
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -23,7 +23,7 @@ export function getIn(
  * @see https://github.com/developit/linkstate
  */
 export function setIn(obj: any, path: string, value: any): any {
-  let res: any = {};
+  let res: any = cloneDeep(obj);
   let resVal: any = res;
   let i = 0;
   let pathArray = toPath(path);
@@ -54,15 +54,13 @@ export function setIn(obj: any, path: string, value: any): any {
     resVal[pathArray[i]] = value;
   }
 
-  const result = { ...obj, ...res };
-
   // If the path array has a single element, the loop did not run.
   // Deleting on `resVal` had no effect in this scenario, so we delete on the result instead.
   if (i === 0 && value === undefined) {
-    delete result[pathArray[i]];
+    delete res[pathArray[i]];
   }
 
-  return result;
+  return res;
 }
 
 /**
diff --git a/tsconfig.storybook.json b/tsconfig.storybook.json
new file mode 100644
index 0000000..413528f
--- /dev/null
+++ b/tsconfig.storybook.json
@@ -0,0 +1,7 @@
+{
+  "extends": "./tsconfig.base.json",
+  "include": ["src", "./typings.d.ts", "types"],
+  "compilerOptions": {
+    "baseUrl": "./"
+  }
+}
\ No newline at end of file


Test patch:
diff --git a/test/utils.test.tsx b/test/utils.test.tsx
index 820eeac..811b022 100755
--- a/test/utils.test.tsx
+++ b/test/utils.test.tsx
@@ -235,6 +235,21 @@ describe('utils', () => {
       expect(obj).toEqual({ x: 'y' });
       expect(newObj).toEqual({ x: 'y', a: { x: { c: 'value' } } });
     });
+
+    it('should keep class inheritance for the top level object', () => {
+      class TestClass {
+        constructor(public key: string, public setObj?: any) {}
+      }
+      const obj = new TestClass('value');
+      const newObj = setIn(obj, 'setObj.nested', 'setInValue');
+      expect(obj).toEqual(new TestClass('value'));
+      expect(newObj).toEqual({
+        key: 'value',
+        setObj: { nested: 'setInValue' },
+      });
+      expect(obj instanceof TestClass).toEqual(true);
+      expect(newObj instanceof TestClass).toEqual(true);
+    });
   });
 
   describe('isPromise', () => {