Repository: reduxjs/redux-toolkit
Pull request #350 (2020-02-10 17:35:33)

correctly enforce meta and error types as returned from `prepare`
This fixes an error where `meta` and `error` could not be typed when using the `prepare` notation of `createSlice`.

netlify[bot]: Deploy preview for *redux-starter-kit-docs* ready!

Built with commit d2be14348534a83cc90b41109a5f72a009d16b6c

https://deploy-preview-350--redux-starter-kit-docs.netlify.com
codesandbox[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/reduxjs/redux-toolkit/pr/350/builds/10520) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 9023d83569a310137e95bd0c7df98126ab62c107:

|Sandbox| Source |
|--|--|
|[naughty-cdn-q87p7](https://codesandbox.io/s/naughty-cdn-q87p7)| Configuration |
|[brave-banach-crjk6](https://codesandbox.io/s/brave-banach-crjk6)| Configuration |
|[great-browser-kbqyx](https://codesandbox.io/s/great-browser-kbqyx)| Configuration |

phryneas: Pre-3.4-TypeScript clearly hates me *sigh*

Patch:
diff --git a/docs/tutorials/intermediate-tutorial.md b/docs/tutorials/intermediate-tutorial.md
index 6e652871..e7fb8bf2 100644
--- a/docs/tutorials/intermediate-tutorial.md
+++ b/docs/tutorials/intermediate-tutorial.md
@@ -146,7 +146,7 @@ export default todos
 We can see that it handles three cases:
 
 - It adds a new todo by copying the existing `state` array and adding a new todo entry at the end
-- It handles toggling a todo entry by copying the existing array use `state.map()`, copies and replaces the todo object that needs to be updated, and leaves all other todo entries alone.
+- It handles toggling a todo entry by copying the existing array using `state.map()`, copies and replaces the todo object that needs to be updated, and leaves all other todo entries alone.
 - It responds to all other actions by returning the existing state (effectively saying "I don't care about that action").
 
 It also initializes the state with a default value of `[]`, and does a default export of the reducer function.
diff --git a/etc/redux-toolkit.api.md b/etc/redux-toolkit.api.md
index 87ba8f41..fa63fde7 100644
--- a/etc/redux-toolkit.api.md
+++ b/etc/redux-toolkit.api.md
@@ -194,21 +194,17 @@ export type SliceActionCreator<P> = PayloadActionCreator<P>;
 
 // @public
 export type SliceCaseReducers<State> = {
-    [K: string]: CaseReducer<State, PayloadAction<any>> | CaseReducerWithPrepare<State, PayloadAction<any>>;
+    [K: string]: CaseReducer<State, PayloadAction<any>> | CaseReducerWithPrepare<State, PayloadAction<any, string, any, any>>;
 };
 
 export { ThunkAction }
 
 // @public
 export type ValidateSliceCaseReducers<S, ACR extends SliceCaseReducers<S>> = ACR & {
-    [P in keyof ACR]: ACR[P] extends {
-        reducer(s: S, action?: {
-            payload: infer O;
-        }): any;
+    [T in keyof ACR]: ACR[T] extends {
+        reducer(s: S, action?: infer A): any;
     } ? {
-        prepare(...a: never[]): {
-            payload: O;
-        };
+        prepare(...a: never[]): Omit<A, 'type'>;
     } : {};
 };
 
diff --git a/src/createSlice.ts b/src/createSlice.ts
index 6dfeefcd..58275d50 100644
--- a/src/createSlice.ts
+++ b/src/createSlice.ts
@@ -12,6 +12,7 @@ import {
   ActionReducerMapBuilder,
   executeReducerBuilderCallback
 } from './mapBuilders'
+import { Omit } from './tsHelpers'
 
 /**
  * An action creator atttached to a slice.
@@ -110,7 +111,7 @@ export type CaseReducerWithPrepare<State, Action extends PayloadAction> = {
 export type SliceCaseReducers<State> = {
   [K: string]:
     | CaseReducer<State, PayloadAction<any>>
-    | CaseReducerWithPrepare<State, PayloadAction<any>>
+    | CaseReducerWithPrepare<State, PayloadAction<any, string, any, any>>
 }
 
 /**
@@ -187,11 +188,11 @@ export type ValidateSliceCaseReducers<
   ACR extends SliceCaseReducers<S>
 > = ACR &
   {
-    [P in keyof ACR]: ACR[P] extends {
-      reducer(s: S, action?: { payload: infer O }): any
+    [T in keyof ACR]: ACR[T] extends {
+      reducer(s: S, action?: infer A): any
     }
       ? {
-          prepare(...a: never[]): { payload: O }
+          prepare(...a: never[]): Omit<A, 'type'>
         }
       : {}
   }
diff --git a/src/tsHelpers.ts b/src/tsHelpers.ts
index bbde02b8..d5dcda74 100644
--- a/src/tsHelpers.ts
+++ b/src/tsHelpers.ts
@@ -87,3 +87,5 @@ type UnionToIntersection<U> = (U extends any
   : never) extends ((k: infer I) => void)
   ? I
   : never
+
+export type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>


Test patch:
diff --git a/type-tests/files/createSlice.typetest.ts b/type-tests/files/createSlice.typetest.ts
index 7a1d7bb4..65f8008b 100644
--- a/type-tests/files/createSlice.typetest.ts
+++ b/type-tests/files/createSlice.typetest.ts
@@ -70,7 +70,7 @@ function expectType<T>(t: T) {
           ? payload.reduce((acc, val) => acc * val, state)
           : state * payload,
       addTwo: {
-        reducer: (s, { payload }) => s + payload,
+        reducer: (s, { payload }: PayloadAction<number>) => s + payload,
         prepare: (a: number, b: number) => ({
           payload: a + b
         })
@@ -175,6 +175,65 @@ function expectType<T>(t: T) {
   expectType<string>(counter.actions.concatMetaStrLen('test').meta)
 }
 
+/**
+ * Test: access meta and error from reducer
+ */
+{
+  const counter = createSlice({
+    name: 'test',
+    initialState: { counter: 0, concat: '' },
+    reducers: {
+      // case: meta and error not used in reducer
+      testDefaultMetaAndError: {
+        reducer(_, action: PayloadAction<number, string>) {},
+        prepare: (payload: number) => ({
+          payload,
+          meta: 'meta' as 'meta',
+          error: 'error' as 'error'
+        })
+      },
+      // case: meta and error marked as "unknown" in reducer
+      testUnknownMetaAndError: {
+        reducer(_, action: PayloadAction<number, string, unknown, unknown>) {},
+        prepare: (payload: number) => ({
+          payload,
+          meta: 'meta' as 'meta',
+          error: 'error' as 'error'
+        })
+      },
+      // case: meta and error are typed in the reducer as returned by prepare
+      testMetaAndError: {
+        reducer(_, action: PayloadAction<number, string, 'meta', 'error'>) {},
+        prepare: (payload: number) => ({
+          payload,
+          meta: 'meta' as 'meta',
+          error: 'error' as 'error'
+        })
+      },
+      // case: meta is typed differently in the reducer than returned from prepare
+      testErroneousMeta: {
+        reducer(_, action: PayloadAction<number, string, 'meta', 'error'>) {},
+        // typings:expect-error
+        prepare: (payload: number) => ({
+          payload,
+          meta: 1,
+          error: 'error' as 'error'
+        })
+      },
+      // case: error is typed differently in the reducer than returned from prepare
+      testErroneousError: {
+        reducer(_, action: PayloadAction<number, string, 'meta', 'error'>) {},
+        // typings:expect-error
+        prepare: (payload: number) => ({
+          payload,
+          meta: 'meta' as 'meta',
+          error: 1
+        })
+      }
+    }
+  })
+}
+
 /*
  * Test: returned case reducer has the correct type
  */