-
Notifications
You must be signed in to change notification settings - Fork 15
/
Copy pathimmer-reducer.ts
337 lines (272 loc) · 9.26 KB
/
immer-reducer.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
import produce, {Draft} from "immer";
let actionTypePrefix = "IMMER_REDUCER";
/** get function arguments as tuple type */
type ArgumentsType<T> = T extends (...args: infer V) => any ? V : never;
/**
* Get the first value of tuple when the tuple length is 1 otherwise return the
* whole tuple
*/
type FirstOrAll<T> = T extends [infer V] ? V : T;
/** Get union of function property names */
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
type MethodObject = {[key: string]: () => any};
/** Pick only methods from object */
type Methods<T> = Pick<T, FunctionPropertyNames<T>>;
/** flatten functions in an object to their return values */
type FlattenToReturnTypes<T extends MethodObject> = {
[K in keyof T]: ReturnType<T[K]>;
};
/** get union of object value types */
type ObjectValueTypes<T> = T[keyof T];
/** get union of object method return types */
type ReturnTypeUnion<T extends MethodObject> = ObjectValueTypes<
FlattenToReturnTypes<T>
>;
/**
* Get union of actions types from a ImmerReducer class
*/
export type Actions<T extends ImmerReducerClass> = ReturnTypeUnion<
ActionCreators<T>
>;
/** type constraint for the ImmerReducer class */
export interface ImmerReducerClass {
customName?: string;
new (...args: any[]): ImmerReducer<any>;
}
/** get state type from a ImmerReducer subclass */
export type ImmerReducerState<T> = T extends {
prototype: {
state: infer V;
};
}
? V
: never;
/** generate reducer function type from the ImmerReducer class */
interface ImmerReducerFunction<T extends ImmerReducerClass> {
(
state: ImmerReducerState<T> | undefined,
action: ReturnTypeUnion<ActionCreators<T>>,
): ImmerReducerState<T>;
}
/** ActionCreator function interface with actual action type name */
interface ImmerActionCreator<ActionTypeType, Payload extends any[]> {
readonly type: ActionTypeType;
(...args: Payload): {
type: ActionTypeType;
payload: FirstOrAll<Payload>;
};
}
/** generate ActionCreators types from the ImmerReducer class */
export type ActionCreators<ClassActions extends ImmerReducerClass> = {
[K in keyof Methods<InstanceType<ClassActions>>]: ImmerActionCreator<
K,
ArgumentsType<InstanceType<ClassActions>[K]>
>;
};
/**
* Internal type for the action
*/
type ImmerAction =
| {
type: string;
payload: unknown;
args?: false;
}
| {
type: string;
payload: unknown[];
args: true;
};
/**
* Type guard for detecting actions created by immer reducer
*
* @param action any redux action
* @param immerActionCreator method from a ImmerReducer class
*/
export function isAction<A extends ImmerActionCreator<any, any>>(
action: {type: any},
immerActionCreator: A,
): action is ReturnType<A> {
return action.type === immerActionCreator.type;
}
function isActionFromClass<T extends ImmerReducerClass>(
action: {type: any},
immerReducerClass: T,
): action is Actions<T> {
if (typeof action.type !== "string") {
return false;
}
if (!action.type.startsWith(actionTypePrefix + ":")) {
return false;
}
const [className, methodName] = removePrefix(action.type).split("#");
if (className !== getReducerName(immerReducerClass)) {
return false;
}
if (typeof immerReducerClass.prototype[methodName] !== "function") {
return false;
}
return true;
}
export function isActionFrom<T extends ImmerReducerClass>(
action: {type: any},
immerReducerClass: T,
): action is Actions<T> {
return isActionFromClass(action, immerReducerClass);
}
/** The actual ImmerReducer class */
export class ImmerReducer<T> {
static customName?: string;
readonly state: T;
draftState: Draft<T>; // Make read only states mutable using Draft
constructor(draftState: Draft<T>, state: T) {
this.state = state;
this.draftState = draftState;
}
}
function removePrefix(actionType: string) {
return actionType
.split(":")
.slice(1)
.join(":");
}
let KNOWN_REDUCER_CLASSES: typeof ImmerReducer[] = [];
const DUPLICATE_INCREMENTS: {[name: string]: number | undefined} = {};
/**
* Set customName for classes automatically if there is multiple reducers
* classes defined with the same name. This can occur accidentaly when using
* name mangling with minifiers.
*
* @param immerReducerClass
*/
function setCustomNameForDuplicates(immerReducerClass: typeof ImmerReducer) {
const hasSetCustomName = KNOWN_REDUCER_CLASSES.find(klass =>
Boolean(klass === immerReducerClass),
);
if (hasSetCustomName) {
return;
}
const duplicateCustomName =
immerReducerClass.customName &&
KNOWN_REDUCER_CLASSES.find(klass =>
Boolean(
klass.customName &&
klass.customName === immerReducerClass.customName,
),
);
if (duplicateCustomName) {
throw new Error(
`There is already customName ${immerReducerClass.customName} defined for ${duplicateCustomName.name}`,
);
}
const duplicate = KNOWN_REDUCER_CLASSES.find(
klass => klass.name === immerReducerClass.name,
);
if (duplicate && !duplicate.customName) {
let number = DUPLICATE_INCREMENTS[immerReducerClass.name];
if (number) {
number++;
} else {
number = 1;
}
DUPLICATE_INCREMENTS[immerReducerClass.name] = number;
immerReducerClass.customName = immerReducerClass.name + "_" + number;
}
KNOWN_REDUCER_CLASSES.push(immerReducerClass);
}
/**
* Convert function arguments to ImmerAction object
*/
function createImmerAction(type: string, args: unknown[]): ImmerAction {
if (args.length === 1) {
return {type, payload: args[0]};
}
return {
type,
payload: args,
args: true,
};
}
/**
* Get function arguments from the ImmerAction object
*/
function getArgsFromImmerAction(action: ImmerAction): unknown[] {
if (action.args) {
return action.payload;
}
return [action.payload];
}
export function createActionCreators<T extends ImmerReducerClass>(
immerReducerClass: T,
): ActionCreators<T> {
setCustomNameForDuplicates(immerReducerClass);
const actionCreators: {[key: string]: Function} = {};
Object.getOwnPropertyNames(immerReducerClass.prototype).forEach(key => {
if (key === "constructor") {
return;
}
const method = immerReducerClass.prototype[key];
if (typeof method !== "function") {
return;
}
const type = `${actionTypePrefix}:${getReducerName(
immerReducerClass,
)}#${key}`;
const actionCreator = (...args: any[]) => {
// Make sure only the arguments are passed to the action object that
// are defined in the method
return createImmerAction(type, args.slice(0, method.length));
};
actionCreator.type = type;
actionCreators[key] = actionCreator;
});
return actionCreators as any; // typed in the function signature
}
function getReducerName(klass: {name: string; customName?: string}) {
return klass.customName || klass.name;
}
export function createReducerFunction<T extends ImmerReducerClass>(
immerReducerClass: T,
initialState?: ImmerReducerState<T>,
): ImmerReducerFunction<T> {
setCustomNameForDuplicates(immerReducerClass);
return function immerReducerFunction(state, action) {
if (state === undefined) {
state = initialState;
}
if (!isActionFromClass(action, immerReducerClass)) {
return state;
}
if (!state) {
throw new Error(
"ImmerReducer does not support undefined state. Pass initial state to createReducerFunction() or createStore()",
);
}
const [_, methodName] = removePrefix(action.type as string).split("#");
return produce(state, draftState => {
const reducers: any = new immerReducerClass(draftState, state);
reducers[methodName](...getArgsFromImmerAction(action as any));
// The reducer replaced the instance with completely new state so
// make that to be the next state
if (reducers.draftState !== draftState) {
return reducers.draftState;
}
// Workaround typing changes in Immer 3.x. This does not actually
// affect the exposed types by immer-reducer itself.
// Also using immer internally with anys like this allow us to
// support multiple versions of immer from 1.4 to 3.x
return draftState as any;
});
};
}
export function setPrefix(prefix: string): void {
actionTypePrefix = prefix;
}
/**
* INTERNAL! This is only for tests!
*/
export function _clearKnownClasses() {
KNOWN_REDUCER_CLASSES = [];
}