From 762f161dea57dd2979a7ae399acd6de70b2e91f9 Mon Sep 17 00:00:00 2001 From: Thibault Gouala Date: Sun, 16 Feb 2020 18:20:26 +0100 Subject: [PATCH] strongly type slice name (#354) * strongly type slice name * move new generic to the end and default it to string --- etc/redux-toolkit.api.md | 10 ++--- src/createSlice.ts | 21 ++++++----- type-tests/files/createSlice.typetest.ts | 47 ++++++++++++++++++++---- 3 files changed, 57 insertions(+), 21 deletions(-) diff --git a/etc/redux-toolkit.api.md b/etc/redux-toolkit.api.md index fa63fde73..f435fb8e0 100644 --- a/etc/redux-toolkit.api.md +++ b/etc/redux-toolkit.api.md @@ -112,13 +112,13 @@ export { createSelector } export function createSerializableStateInvariantMiddleware(options?: SerializableStateInvariantMiddlewareOptions): Middleware; // @public -export function createSlice>(options: CreateSliceOptions): Slice; +export function createSlice, Name extends string = string>(options: CreateSliceOptions): Slice; // @public -export interface CreateSliceOptions = SliceCaseReducers> { +export interface CreateSliceOptions = SliceCaseReducers, Name extends string = string> { extraReducers?: CaseReducers, any> | ((builder: ActionReducerMapBuilder>) => void); initialState: State; - name: string; + name: Name; reducers: ValidateSliceCaseReducers; } @@ -182,10 +182,10 @@ export interface SerializableStateInvariantMiddlewareOptions { } // @public -export interface Slice = SliceCaseReducers> { +export interface Slice = SliceCaseReducers, Name extends string = string> { actions: CaseReducerActions; caseReducers: SliceDefinedCaseReducers; - name: string; + name: Name; reducer: Reducer; } diff --git a/src/createSlice.ts b/src/createSlice.ts index 58275d503..8b4e9c530 100644 --- a/src/createSlice.ts +++ b/src/createSlice.ts @@ -1,13 +1,13 @@ import { Reducer } from 'redux' import { + ActionCreatorWithoutPayload, createAction, PayloadAction, PayloadActionCreator, PrepareAction, - ActionCreatorWithoutPayload, _ActionCreatorWithPreparedPayload } from './createAction' -import { createReducer, CaseReducers, CaseReducer } from './createReducer' +import { CaseReducer, CaseReducers, createReducer } from './createReducer' import { ActionReducerMapBuilder, executeReducerBuilderCallback @@ -30,12 +30,13 @@ export type SliceActionCreator

= PayloadActionCreator

*/ export interface Slice< State = any, - CaseReducers extends SliceCaseReducers = SliceCaseReducers + CaseReducers extends SliceCaseReducers = SliceCaseReducers, + Name extends string = string > { /** * The slice name. */ - name: string + name: Name /** * The slice's reducer. @@ -62,12 +63,13 @@ export interface Slice< */ export interface CreateSliceOptions< State = any, - CR extends SliceCaseReducers = SliceCaseReducers + CR extends SliceCaseReducers = SliceCaseReducers, + Name extends string = string > { /** * The slice's name. Used to namespace the generated action types. */ - name: string + name: Name /** * The initial state to be returned by the slice reducer. @@ -213,10 +215,11 @@ function getType(slice: string, actionKey: string): string { */ export function createSlice< State, - CaseReducers extends SliceCaseReducers + CaseReducers extends SliceCaseReducers, + Name extends string = string >( - options: CreateSliceOptions -): Slice { + options: CreateSliceOptions +): Slice { const { name, initialState } = options if (!name) { throw new Error('`name` is a required option for createSlice') diff --git a/type-tests/files/createSlice.typetest.ts b/type-tests/files/createSlice.typetest.ts index 65f8008bc..ea459e1cd 100644 --- a/type-tests/files/createSlice.typetest.ts +++ b/type-tests/files/createSlice.typetest.ts @@ -1,22 +1,55 @@ -import { AnyAction, Reducer, Action } from 'redux' +import { Action, AnyAction, Reducer } from 'redux' +import { ValidateSliceCaseReducers } from 'src/createSlice' import { - createSlice, - PayloadAction, - createAction, - ActionReducerMapBuilder, - ActionCreatorWithOptionalPayload, ActionCreatorWithNonInferrablePayload, + ActionCreatorWithOptionalPayload, ActionCreatorWithoutPayload, ActionCreatorWithPayload, ActionCreatorWithPreparedPayload, + ActionReducerMapBuilder, + createAction, + createSlice, + PayloadAction, SliceCaseReducers } from '../../src' -import { ValidateSliceCaseReducers } from 'src/createSlice' function expectType(t: T) { return t } +/* + * Test: Slice name is strongly typed. + */ + +const counterSlice = createSlice({ + name: 'counter', + initialState: 0, + reducers: { + increment: (state: number, action) => state + action.payload, + decrement: (state: number, action) => state - action.payload + } +}) + +const uiSlice = createSlice({ + name: 'ui', + initialState: 0, + reducers: { + goToNext: (state: number, action) => state + action.payload, + goToPrevious: (state: number, action) => state - action.payload + } +}) + +const actionCreators = { + [counterSlice.name]: { ...counterSlice.actions }, + [uiSlice.name]: { ...uiSlice.actions } +} + +expectType(actionCreators.counter) +expectType(actionCreators.ui) + +// typings:expect-error +const value = actionCreators.anyKey + /* * Test: createSlice() infers the returned slice's type. */