UNPKG

9.52 kBPlain TextView Raw
1import { isAction } from 'redux'
2import type {
3 IsUnknownOrNonInferrable,
4 IfMaybeUndefined,
5 IfVoid,
6 IsAny,
7} from './tsHelpers'
8import { hasMatchFunction } from './tsHelpers'
9
10/**
11 * An action with a string type and an associated payload. This is the
12 * type of action returned by `createAction()` action creators.
13 *
14 * @template P The type of the action's payload.
15 * @template T the type used for the action type.
16 * @template M The type of the action's meta (optional)
17 * @template E The type of the action's error (optional)
18 *
19 * @public
20 */
21export type PayloadAction<
22 P = void,
23 T extends string = string,
24 M = never,
25 E = never,
26> = {
27 payload: P
28 type: T
29} & ([M] extends [never]
30 ? {}
31 : {
32 meta: M
33 }) &
34 ([E] extends [never]
35 ? {}
36 : {
37 error: E
38 })
39
40/**
41 * A "prepare" method to be used as the second parameter of `createAction`.
42 * Takes any number of arguments and returns a Flux Standard Action without
43 * type (will be added later) that *must* contain a payload (might be undefined).
44 *
45 * @public
46 */
47export type PrepareAction<P> =
48 | ((...args: any[]) => { payload: P })
49 | ((...args: any[]) => { payload: P; meta: any })
50 | ((...args: any[]) => { payload: P; error: any })
51 | ((...args: any[]) => { payload: P; meta: any; error: any })
52
53/**
54 * Internal version of `ActionCreatorWithPreparedPayload`. Not to be used externally.
55 *
56 * @internal
57 */
58export type _ActionCreatorWithPreparedPayload<
59 PA extends PrepareAction<any> | void,
60 T extends string = string,
61> =
62 PA extends PrepareAction<infer P>
63 ? ActionCreatorWithPreparedPayload<
64 Parameters<PA>,
65 P,
66 T,
67 ReturnType<PA> extends {
68 error: infer E
69 }
70 ? E
71 : never,
72 ReturnType<PA> extends {
73 meta: infer M
74 }
75 ? M
76 : never
77 >
78 : void
79
80/**
81 * Basic type for all action creators.
82 *
83 * @inheritdoc {redux#ActionCreator}
84 */
85export interface BaseActionCreator<P, T extends string, M = never, E = never> {
86 type: T
87 match: (action: unknown) => action is PayloadAction<P, T, M, E>
88}
89
90/**
91 * An action creator that takes multiple arguments that are passed
92 * to a `PrepareAction` method to create the final Action.
93 * @typeParam Args arguments for the action creator function
94 * @typeParam P `payload` type
95 * @typeParam T `type` name
96 * @typeParam E optional `error` type
97 * @typeParam M optional `meta` type
98 *
99 * @inheritdoc {redux#ActionCreator}
100 *
101 * @public
102 */
103export interface ActionCreatorWithPreparedPayload<
104 Args extends unknown[],
105 P,
106 T extends string = string,
107 E = never,
108 M = never,
109> extends BaseActionCreator<P, T, M, E> {
110 /**
111 * Calling this {@link redux#ActionCreator} with `Args` will return
112 * an Action with a payload of type `P` and (depending on the `PrepareAction`
113 * method used) a `meta`- and `error` property of types `M` and `E` respectively.
114 */
115 (...args: Args): PayloadAction<P, T, M, E>
116}
117
118/**
119 * An action creator of type `T` that takes an optional payload of type `P`.
120 *
121 * @inheritdoc {redux#ActionCreator}
122 *
123 * @public
124 */
125export interface ActionCreatorWithOptionalPayload<P, T extends string = string>
126 extends BaseActionCreator<P, T> {
127 /**
128 * Calling this {@link redux#ActionCreator} with an argument will
129 * return a {@link PayloadAction} of type `T` with a payload of `P`.
130 * Calling it without an argument will return a PayloadAction with a payload of `undefined`.
131 */
132 (payload?: P): PayloadAction<P, T>
133}
134
135/**
136 * An action creator of type `T` that takes no payload.
137 *
138 * @inheritdoc {redux#ActionCreator}
139 *
140 * @public
141 */
142export interface ActionCreatorWithoutPayload<T extends string = string>
143 extends BaseActionCreator<undefined, T> {
144 /**
145 * Calling this {@link redux#ActionCreator} will
146 * return a {@link PayloadAction} of type `T` with a payload of `undefined`
147 */
148 (noArgument: void): PayloadAction<undefined, T>
149}
150
151/**
152 * An action creator of type `T` that requires a payload of type P.
153 *
154 * @inheritdoc {redux#ActionCreator}
155 *
156 * @public
157 */
158export interface ActionCreatorWithPayload<P, T extends string = string>
159 extends BaseActionCreator<P, T> {
160 /**
161 * Calling this {@link redux#ActionCreator} with an argument will
162 * return a {@link PayloadAction} of type `T` with a payload of `P`
163 */
164 (payload: P): PayloadAction<P, T>
165}
166
167/**
168 * An action creator of type `T` whose `payload` type could not be inferred. Accepts everything as `payload`.
169 *
170 * @inheritdoc {redux#ActionCreator}
171 *
172 * @public
173 */
174export interface ActionCreatorWithNonInferrablePayload<
175 T extends string = string,
176> extends BaseActionCreator<unknown, T> {
177 /**
178 * Calling this {@link redux#ActionCreator} with an argument will
179 * return a {@link PayloadAction} of type `T` with a payload
180 * of exactly the type of the argument.
181 */
182 <PT extends unknown>(payload: PT): PayloadAction<PT, T>
183}
184
185/**
186 * An action creator that produces actions with a `payload` attribute.
187 *
188 * @typeParam P the `payload` type
189 * @typeParam T the `type` of the resulting action
190 * @typeParam PA if the resulting action is preprocessed by a `prepare` method, the signature of said method.
191 *
192 * @public
193 */
194export type PayloadActionCreator<
195 P = void,
196 T extends string = string,
197 PA extends PrepareAction<P> | void = void,
198> = IfPrepareActionMethodProvided<
199 PA,
200 _ActionCreatorWithPreparedPayload<PA, T>,
201 // else
202 IsAny<
203 P,
204 ActionCreatorWithPayload<any, T>,
205 IsUnknownOrNonInferrable<
206 P,
207 ActionCreatorWithNonInferrablePayload<T>,
208 // else
209 IfVoid<
210 P,
211 ActionCreatorWithoutPayload<T>,
212 // else
213 IfMaybeUndefined<
214 P,
215 ActionCreatorWithOptionalPayload<P, T>,
216 // else
217 ActionCreatorWithPayload<P, T>
218 >
219 >
220 >
221 >
222>
223
224/**
225 * A utility function to create an action creator for the given action type
226 * string. The action creator accepts a single argument, which will be included
227 * in the action object as a field called payload. The action creator function
228 * will also have its toString() overridden so that it returns the action type.
229 *
230 * @param type The action type to use for created actions.
231 * @param prepare (optional) a method that takes any number of arguments and returns { payload } or { payload, meta }.
232 * If this is given, the resulting action creator will pass its arguments to this method to calculate payload & meta.
233 *
234 * @public
235 */
236export function createAction<P = void, T extends string = string>(
237 type: T,
238): PayloadActionCreator<P, T>
239
240/**
241 * A utility function to create an action creator for the given action type
242 * string. The action creator accepts a single argument, which will be included
243 * in the action object as a field called payload. The action creator function
244 * will also have its toString() overridden so that it returns the action type.
245 *
246 * @param type The action type to use for created actions.
247 * @param prepare (optional) a method that takes any number of arguments and returns { payload } or { payload, meta }.
248 * If this is given, the resulting action creator will pass its arguments to this method to calculate payload & meta.
249 *
250 * @public
251 */
252export function createAction<
253 PA extends PrepareAction<any>,
254 T extends string = string,
255>(
256 type: T,
257 prepareAction: PA,
258): PayloadActionCreator<ReturnType<PA>['payload'], T, PA>
259
260export function createAction(type: string, prepareAction?: Function): any {
261 function actionCreator(...args: any[]) {
262 if (prepareAction) {
263 let prepared = prepareAction(...args)
264 if (!prepared) {
265 throw new Error('prepareAction did not return an object')
266 }
267
268 return {
269 type,
270 payload: prepared.payload,
271 ...('meta' in prepared && { meta: prepared.meta }),
272 ...('error' in prepared && { error: prepared.error }),
273 }
274 }
275 return { type, payload: args[0] }
276 }
277
278 actionCreator.toString = () => `${type}`
279
280 actionCreator.type = type
281
282 actionCreator.match = (action: unknown): action is PayloadAction =>
283 isAction(action) && action.type === type
284
285 return actionCreator
286}
287
288/**
289 * Returns true if value is an RTK-like action creator, with a static type property and match method.
290 */
291export function isActionCreator(
292 action: unknown,
293): action is BaseActionCreator<unknown, string> & Function {
294 return (
295 typeof action === 'function' &&
296 'type' in action &&
297 // hasMatchFunction only wants Matchers but I don't see the point in rewriting it
298 hasMatchFunction(action as any)
299 )
300}
301
302/**
303 * Returns true if value is an action with a string type and valid Flux Standard Action keys.
304 */
305export function isFSA(action: unknown): action is {
306 type: string
307 payload?: unknown
308 error?: unknown
309 meta?: unknown
310} {
311 return isAction(action) && Object.keys(action).every(isValidKey)
312}
313
314function isValidKey(key: string) {
315 return ['type', 'payload', 'error', 'meta'].indexOf(key) > -1
316}
317
318// helper types for more readable typings
319
320type IfPrepareActionMethodProvided<
321 PA extends PrepareAction<any> | void,
322 True,
323 False,
324> = PA extends (...args: any[]) => any ? True : False