Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor typings for readability #168

Merged
merged 3 commits into from
Sep 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could go and rename P to payload, T to Type and PA to PrepareActionFn here, but I'm not really sure if it makes things better or worse in the usages of these below.
Do you have an opinion there?

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