Skip to content

Commit

Permalink
refactor typings for readability (#168)
Browse files Browse the repository at this point in the history
* refactor createActions types to be more readable

* refactor createSlice types to be more readable

* add else comments
  • Loading branch information
phryneas authored and markerikson committed Sep 7, 2019
1 parent af6d05a commit 85923d4
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 66 deletions.
82 changes: 54 additions & 28 deletions src/createAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,51 +6,55 @@ import { Action } from 'redux'
*
* @template P The type of the action's payload.
* @template T the type used for the action type.
* @template M The type of the action's meta (optional)
*/
export type PayloadAction<
P = any,
T extends string = string,
M = void
> = Action<T> & {
payload: P
} & ([M] extends [void] ? {} : { meta: M })

export type Diff<T, U> = T extends U ? never : T
> = WithOptionalMeta<M, WithPayload<P, Action<T>>>;

export type PrepareAction<P> =
| ((...args: any[]) => { payload: P })
| ((...args: any[]) => { payload: P; meta: any })


export type ActionCreatorWithPreparedPayload<PA extends PrepareAction<any> | void, T extends string = string> =
WithTypeProperty<T, PA extends PrepareAction<infer P> ? (...args: Parameters<PA>) => PayloadAction<P, T, MetaOrVoid<PA>> : void>;

export type ActionCreatorWithOptionalPayload<P, T extends string = string> =
WithTypeProperty<T, {
(payload?: undefined): PayloadAction<undefined, T>
<PT extends Diff<P, undefined>>(payload?: PT): PayloadAction<PT, T>
}>;

export type ActionCreatorWithoutPayload<T extends string = string> = WithTypeProperty<T, () => PayloadAction<undefined, T>>;

export type ActionCreatorWithPayload<P, T extends string = string> =
WithTypeProperty<T, <PT extends P>(payload: PT) => PayloadAction<PT, T>>;

/**
* An action creator that produces actions with a `payload` attribute.
*/
export type PayloadActionCreator<
P = any,
T extends string = string,
PA extends PrepareAction<P> | void = void
> = {
type: T
} & (PA extends (...args: any[]) => any
? (ReturnType<PA> extends { meta: infer M }
? (...args: Parameters<PA>) => PayloadAction<P, T, M>
: (...args: Parameters<PA>) => PayloadAction<P, T>)
: (/*
* The `P` generic is wrapped with a single-element tuple to prevent the
* conditional from being checked distributively, thus preserving unions
* of contra-variant types.
*/
[undefined] extends [P]
? {
(payload?: undefined): PayloadAction<undefined, T>
<PT extends Diff<P, undefined>>(payload?: PT): PayloadAction<PT, T>
}
: [void] extends [P]
? {
(): PayloadAction<undefined, T>
}
: {
<PT extends P>(payload: PT): PayloadAction<PT, T>
}))
> =
IfPrepareActionMethodProvided<PA,
ActionCreatorWithPreparedPayload<PA, T>,
// else
IfMaybeUndefined<P,
ActionCreatorWithOptionalPayload<P, T>,
// else
IfVoid<P,
ActionCreatorWithoutPayload<T>,
// else
ActionCreatorWithPayload<P, T>
>
>
>
;

/**
* A utility function to create an action creator for the given action type
Expand All @@ -60,6 +64,8 @@ export type PayloadActionCreator<
* allowing it to be used in reducer logic that is looking for that action type.
*
* @param type The action type to use for created actions.
* @param prepare (optional) a method that takes any number of arguments and returns { payload } or { payload, meta }.
* If this is given, the resulting action creator will pass it's arguments to this method to calculate payload & meta.
*/

export function createAction<P = any, T extends string = string>(
Expand Down Expand Up @@ -108,3 +114,23 @@ export function getType<T extends string>(
): T {
return `${actionCreator}` as T
}

// helper types for more readable typings

type Diff<T, U> = T extends U ? never : T

type WithPayload<P, T> = T & { payload: P };

type WithOptionalMeta<M, T> = T & ([M] extends [void] ? {} : { meta: M })

type WithTypeProperty<T, MergeIn> = {
type: T
} & MergeIn;

type IfPrepareActionMethodProvided<PA extends PrepareAction<any> | void, True, False> = PA extends (...args: any[]) => any ? True : False;

type MetaOrVoid<PA extends PrepareAction<any>> = (ReturnType<PA> extends { meta: infer M } ? M : void);

type IfMaybeUndefined<P, True, False> = [undefined] extends [P] ? True : False;

type IfVoid<P, True, False> = [void] extends [P] ? True : False;
93 changes: 55 additions & 38 deletions src/createSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import {
createAction,
PayloadAction,
PayloadActionCreator,
PrepareAction
PrepareAction,
ActionCreatorWithoutPayload,
ActionCreatorWithPreparedPayload
} from './createAction'
import { createReducer, CaseReducers, CaseReducer } from './createReducer'
import { createSliceSelector, createSelectorName } from './sliceSelector'
Expand All @@ -16,9 +18,9 @@ import { createSliceSelector, createSelectorName } from './sliceSelector'
export type SliceActionCreator<P> = PayloadActionCreator<P>

export interface Slice<
S = any,
AC extends { [key: string]: any } = { [key: string]: any }
> {
State = any,
ActionCreators extends { [key: string]: any } = { [key: string]: any }
> {
/**
* The slice name.
*/
Expand All @@ -27,30 +29,30 @@ export interface Slice<
/**
* The slice's reducer.
*/
reducer: Reducer<S>
reducer: Reducer<State>

/**
* Action creators for the types of actions that are handled by the slice
* reducer.
*/
actions: AC
actions: ActionCreators

/**
* Selectors for the slice reducer state. `createSlice()` inserts a single
* selector that returns the entire slice state and whose name is
* automatically derived from the slice name (e.g., `getCounter` for a slice
* named `counter`).
*/
selectors: { [key: string]: (state: any) => S }
selectors: { [key: string]: (state: any) => State }
}

/**
* Options for `createSlice()`.
*/
export interface CreateSliceOptions<
S = any,
CR extends SliceCaseReducers<S, any> = SliceCaseReducers<S, any>
> {
State = any,
CR extends SliceCaseReducers<State, any> = SliceCaseReducers<State, any>
> {
/**
* The slice's name. Used to namespace the generated action types and to
* name the selector for retrieving the reducer's state.
Expand All @@ -60,7 +62,7 @@ export interface CreateSliceOptions<
/**
* The initial state to be returned by the slice reducer.
*/
initialState: S
initialState: State

/**
* A mapping from action types to action-type-specific *case reducer*
Expand All @@ -74,41 +76,54 @@ export interface CreateSliceOptions<
* functions. These reducers should have existing action types used
* as the keys, and action creators will _not_ be generated.
*/
extraReducers?: CaseReducers<S, any>
extraReducers?: CaseReducers<State, any>
}

type PayloadActions<T extends keyof any = string> = Record<T, PayloadAction>
type PayloadActions<Types extends keyof any = string> = Record<Types, PayloadAction>

type EnhancedCaseReducer<S, A extends PayloadAction> = {
reducer: CaseReducer<S, A>
prepare: PrepareAction<A['payload']>
type EnhancedCaseReducer<State, Action extends PayloadAction> = {
reducer: CaseReducer<State, Action>
prepare: PrepareAction<Action['payload']>
}

type SliceCaseReducers<S, PA extends PayloadActions> = {
[T in keyof PA]: CaseReducer<S, PA[T]> | EnhancedCaseReducer<S, PA[T]>
type SliceCaseReducers<State, PA extends PayloadActions> = {
[ActionType in keyof PA]: CaseReducer<State, PA[ActionType]> | EnhancedCaseReducer<State, PA[ActionType]>
}

type CaseReducerActions<CR extends SliceCaseReducers<any, any>> = {
[T in keyof CR]: CR[T] extends (state: any) => any
? PayloadActionCreator<void>
: (CR[T] extends (state: any, action: PayloadAction<infer P>) => any
? PayloadActionCreator<P>
: CR[T] extends { prepare: PrepareAction<infer P> }
? PayloadActionCreator<P, string, CR[T]['prepare']>
: PayloadActionCreator<void>)
type IfIsReducerFunctionWithoutAction<R, True, False = never> = R extends (state: any) => any ? True : False;
type IfIsEnhancedReducer<R, True, False = never> = R extends { prepare: Function } ? True : False;

type PayloadForReducer<R> = R extends (state: any, action: PayloadAction<infer P>) => any ? P : void;
type PrepareActionForReducer<R> = R extends { prepare: infer Prepare } ? Prepare : never;

type CaseReducerActions<CaseReducers extends SliceCaseReducers<any, any>> = {
[Type in keyof CaseReducers]:
IfIsEnhancedReducer<CaseReducers[Type],
ActionCreatorWithPreparedPayload<PrepareActionForReducer<CaseReducers[Type]>>,
// else
IfIsReducerFunctionWithoutAction<CaseReducers[Type],
ActionCreatorWithoutPayload,
// else
PayloadActionCreator<PayloadForReducer<CaseReducers[Type]>>
>
>
}

type NoInfer<T> = [T][T extends any ? 0 : never];

type SliceCaseReducersCheck<S, ACR> = {
[P in keyof ACR] : ACR[P] extends {
reducer(s:S, action?: { payload: infer O }): any
} ? {
prepare(...a:never[]): { payload: O }
} : {
[P in keyof ACR]: ACR[P] extends {
reducer(s: S, action?: { payload: infer O }): any
} ? {
prepare(...a: never[]): { payload: O }
} : {

}
}
}

type RestrictEnhancedReducersToMatchReducerAndPrepare<S, CR extends SliceCaseReducers<S, any>> =
{ reducers: SliceCaseReducersCheck<S, NoInfer<CR>> };

function getType(slice: string, actionKey: string): string {
return slice ? `${slice}/${actionKey}` : actionKey
}
Expand All @@ -121,12 +136,14 @@ function getType(slice: string, actionKey: string): string {
*
* The `reducer` argument is passed to `createReducer()`.
*/
export function createSlice<S, CR extends SliceCaseReducers<S, any>>(
options: CreateSliceOptions<S, CR> & { reducers: SliceCaseReducersCheck<S, NoInfer<CR>> }
): Slice<S, CaseReducerActions<CR>>
export function createSlice<S, CR extends SliceCaseReducers<S, any>>(
options: CreateSliceOptions<S, CR>
): Slice<S, CaseReducerActions<CR>> {
export function createSlice<State, CaseReducers extends SliceCaseReducers<State, any>>(
options: CreateSliceOptions<State, CaseReducers> & RestrictEnhancedReducersToMatchReducerAndPrepare<State, CaseReducers>
): Slice<State, CaseReducerActions<CaseReducers>>

// internal definition is a little less restrictive
export function createSlice<State, CaseReducers extends SliceCaseReducers<State, any>>(
options: CreateSliceOptions<State, CaseReducers>
): Slice<State, CaseReducerActions<CaseReducers>> {
const { slice = '', initialState } = options
const reducers = options.reducers || {}
const extraReducers = options.extraReducers || {}
Expand Down

0 comments on commit 85923d4

Please sign in to comment.