diff --git a/README.md b/README.md index 62c126e..838d55d 100644 --- a/README.md +++ b/README.md @@ -53,19 +53,20 @@ Issues can be funded by anyone and the money will be transparently distributed t * [Motivation](#motivation) * [Behold the Mighty "Tutorial"](#behold-the-mighty-tutorial) * [API Docs](#api-docs) - * utility-types - * [`ActionType`](#actiontype) (RootAction type-helper) - * [`StateType`](#statetype) (RootState type-helper) - * action-creators - * [`action`](#action) + * Type-helpers + * [`ActionType`](#actiontype) + * [`StateType`](#statetype) + * Action-creators * [`createAction`](#createaction) * [`createStandardAction`](#createstandardaction) + * [`createCustomAction`](#createcustomaction) * [`createAsyncAction`](#createasyncaction) - * [`createActionWithType`](#createactionwithtype) - * action-helpers + * Action-helpers * [`getType`](#gettype) * [`isActionOf`](#isactionof) * [`isOfType`](#isoftype) + * Action-factory + * [`action`](#action) * [Migration Guides](#migration-guides) * [v1.x.x to v2.x.x](#v1xx-to-v2xx) * [Migrating from redux-actions](#migrating-from-redux-actions) @@ -366,27 +367,30 @@ if (isOfType(types.ADD, action)) { ## API Docs -### ActionType +### Type-helpers +Below helper functions are very flexible generalizations, works great with nested structures and will cover numerous different use-cases. -> powerful type helper that will infer union type from "action-creator map" object or "module import" +#### ActionType -_NB: This helper works similar to `ReturnType` but instead of function type parameter it will accept "typeof action-creators" (it can be "import *" from module or "action-creators map")_ +_Powerful type-helper that will infer union type from **import * as ...** or **action-creator map** object._ ```ts import { ActionType } from 'typesafe-actions'; +// with "import * as ..." import * as todos from './actions'; export type TodosAction = ActionType; +// TodosAction: { type: 'action1' } | { type: 'action2' } | { type: 'action3' } - +// with nested action-creator map case const actions = { action1: createAction('action1'), nested: { action2: createAction('action2'), moreNested: { action3: createAction('action3'), - } - } + }, + }, }; export type RootAction = ActionType; // RootAction: { type: 'action1' } | { type: 'action2' } | { type: 'action3' } @@ -396,19 +400,17 @@ export type RootAction = ActionType; --- -### StateType +#### StateType -> powerful type helper that will infer state object type from "reducer function" or "nested/combined reducers" +_Powerful type helper that will infer state object type from **reducer function** and **nested/combined reducers**._ - -_NB: This helper works similar to `ReturnType` but instead of function type parameter it will accept "typeof reducer" or "nested/combined reducers map" (result of `combineReducers`)_ - -> _Redux Compatibility: working with redux@4+ types_ +_**Redux compatibility**: working with redux@4+ types_ ```ts import { combineReducers } from 'redux'; import { StateType } from 'typesafe-actions'; +// with reducer function const todosReducer = (state: Todo[] = [], action: TodosAction) => { switch (action.type) { case getType(todos.add): @@ -416,6 +418,7 @@ const todosReducer = (state: Todo[] = [], action: TodosAction) => { ... export type TodosState = StateType; +// with nested/combined reducers const rootReducer = combineReducers({ router: routerReducer, counters: countersReducer, @@ -427,64 +430,19 @@ export type RootState = StateType; --- -### action - -> simple action factory function, to create typed action - -**Warning**: this action creator does not let you use action helpers such as `getType` and `isActionOf` - -```ts -function action(type: T, payload?: P, meta?: M): { type: T, payload?: P, meta?: M } -``` - -Examples: -[> Advanced Usage Examples](src/action.spec.ts) - -```ts -// type with payload -const createUser = (id: number, name: string) => - action('CREATE_USER', { id, name }); -// { type: 'CREATE_USER'; payload: { id: number; name: string }; } - -// type with meta -const getUsers = (meta: string) => - action('GET_USERS', undefined, meta); -// { type: 'GET_USERS'; meta: string; } -``` - -[⇧ back to top](#table-of-contents) - ---- +### Action-creators -### createAction +#### createAction -> create custom action-creator using constructor function with injected resolver callback +_Create an enhanced action-creator with unlimited number of arguments._ +- Arguments of resulting action-creator will preserve their original semantic names `(id, firstName, lastName)`. +- Returned action objects have predefined properties `({ payload, meta })` ```ts -// type only -function createAction(type: T): () => { type: T }; -// createAction('INCREMENT'); - -// type with payload -function createAction(type: T, executor): (...args) => { type: T, payload: P }; -const executor = (resolve) => (...args) => resolve(payload: P) -// createAction('ADD', resolve => { -// return (amount: number) => resolve(amount); -// }); - -// type with meta -function createAction(type: T, executor): (...args) => { type: T, meta: M }; -const executor = (resolve) => (...args) => resolve(payload: undefined, meta: M) -// createAction('ADD', resolve => { -// return (meta: string) => resolve(undefined, meta); -// }); - -// type with payload and meta -function createAction(type: T, executor): (...args) => { type: T, payload: P, meta: M }; -const executor = (resolve) => (...args) => resolve(payload: P, meta: M) -// createAction('GET_TODO', resolve => { -// return (id: string, meta: string) => resolve(id, meta); -// }); +createAction(type) +createAction(type, actionCallback => { + return (namedArg1, namedArg2, ...namedArgN) => actionCallback(payload?, meta?) +}) ``` Examples: @@ -493,44 +451,45 @@ Examples: ```ts import { createAction } from 'typesafe-actions'; -// type only +// Using action callback +// - with type only const increment = createAction('INCREMENT'); -expect(increment()) - .toEqual({ type: 'INCREMENT' }); +increment(); // { type: 'INCREMENT' }; -// type with payload -const add = createAction('ADD', resolve => { - return (amount: number) => resolve(amount); +// - with type and payload +const add = createAction('ADD', action => { + return (amount: number) => action(amount); }); -expect(add(10)) - .toEqual({ type: 'ADD', payload: 10 }); +add(10); // { type: 'ADD', payload: number } -// type with meta -const getTodos = createAction('GET_TODOS', resolve => { - return (meta: string) => resolve(undefined, meta); +// - with type and meta +const getTodos = createAction('GET_TODOS', action => { + return (params: Params) => action(undefined, params); }); -expect(getTodos('some_meta')) - .toEqual({ type: 'GET_TODOS', meta: 'some_meta' }); +getTodos('some_meta'); // { type: 'GET_TODOS', meta: Params } -// type with payload and meta -const getTodo = createAction('GET_TODO', resolve => { - return (id: string, meta: string) => resolve(id, meta); +// - and finally with type, payload and meta +const getTodo = createAction('GET_TODO', action => { + return (id: string, meta: string) => action(id, meta); }); -expect(getTodo('some_id', 'some_meta')) - .toEqual({ type: 'GET_TODO', payload: 'some_id', meta: 'some_meta' }); +getTodo('some_id', 'some_meta'); // { type: 'GET_TODO', payload: string, meta: string } ``` [⇧ back to top](#table-of-contents) --- -### createStandardAction +#### createStandardAction -> create action-creator that will create "Flux Standard Action" compatible actions to reduce boilerplate and enforce convention +_Create an enhanced action-creator compatible with [Flux Standard Action](https://github.com/redux-utilities/flux-standard-action) to reduce boilerplate and enforce convention._ +- Arguments of resulting action-creator are predefined `(payload, meta)` +- Returned action objects have predefined properties `({ payload, meta })` +- But it also contains a `.map()` method that allow to map `(payload, meta)` arguments to a custom action object `({ customProp1, customProp2, ...customPropN })` ```ts -function createStandardAction(type: T): () => (payload: P, meta: M) => { type: T, payload: P, meta: M }; -function createStandardAction(type: T): { map: (payload: P, meta: M): { ...anything } => (...args) => { type: T, ...anything } }; +createStandardAction(type)() +createStandardAction(type)() +createStandardAction(type).map((payload, meta) => ({ customProp1, customProp2, ...customPropN })) ``` Examples: @@ -539,207 +498,190 @@ Examples: ```ts import { createStandardAction } from 'typesafe-actions'; -// type only +// Very concise with use of generic type arguments +// - with type only +const increment = createStandardAction('INCREMENT')(); const increment = createStandardAction('INCREMENT')(); -expect(increment()).toEqual({ type: 'INCREMENT' }); +increment(); // { type: 'INCREMENT' } -// type with payload +// - with type and payload const add = createStandardAction('ADD')(); -expect(add(10)).toEqual({ type: 'ADD', payload: 10 }); +add(10); // { type: 'INCREMENT', payload: number } -// type with meta +// - with type and meta const getData = createStandardAction('GET_DATA')(); -expect(getData(undefined, 'meta')).toEqual({ type: 'GET_DATA', meta: 'meta' }); +getData(undefined, 'meta'); // { type: 'INCREMENT', meta: string } + +// - and finally with type, payload and meta +const getData = createStandardAction('GET_DATA')(); +getData(1, 'meta'); // { type: 'INCREMENT', payload: number, meta: string } -// type with payload and meta +// Can map payload and meta arguments to a custom action object const notify = createStandardAction('NOTIFY').map( - ({ username, message }}: Notification) => ({ - payload: `${username}: ${message || ''}`, - meta: { username, message }, + (payload: string, meta: Meta) => ({ + from: meta.username, + message: `${username}: ${payload}`, + messageType: meta.type, + datetime: new Date(), }) ); -expect(notify({ username: 'Piotr', message: 'Hello!' })).toEqual({ - type: 'NOTIFY', - payload: 'Piotr: Hello!', - meta: { username: 'Piotr', message: 'Hello!' }, -}); + +notify('Hello!', { username: 'Piotr', type: 'announcement' }); +// { type: 'NOTIFY', from: string, message: string, messageType: MessageType, datetime: Date } ``` [⇧ back to top](#table-of-contents) --- -### createAsyncAction +#### createCustomAction -> create a composite action-creator containing three action handlers for async flow (e.g. network request - request/success/failure) +_Create an enhanced action-creator with unlimited number of arguments and custom properties on action object._ +- Arguments of resulting action-creator will preserve their original semantic names `(id, firstName, lastName)`. +- Returned action objects have custom properties `({ type, customProp1, customProp2, ...customPropN })` ```ts -function createAsyncAction(requestType: T1, successType: T2, failureType: T3): () => { - request: AC, - success: AC, - failure: AC, -}; +createCustomAction(type, type => { + return (namedArg1, namedArg2, ...namedArgN) => ({ type, customProp1, customProp2, ...customPropN }) +}) ``` Examples: -[> Advanced Usage Examples](src/create-async-action.spec.ts) +[> Advanced Usage Examples](src/create-action-with-type.spec.ts) ```ts -import { createAsyncAction } from 'typesafe-actions'; - -const fetchUsers = createAsyncAction( - 'FETCH_USERS_REQUEST', - 'FETCH_USERS_SUCCESS', - 'FETCH_USERS_FAILURE' -)(); - -const requestResult = fetchUsers.request(); -expect(requestResult).toEqual({ - type: 'FETCH_USERS_REQUEST', -}); +import { createCustomAction } from 'typesafe-actions'; -const successResult = fetchUsers.success([{ firstName: 'Piotr', lastName: 'Witek' }]); -expect(successResult).toEqual({ - type: 'FETCH_USERS_SUCCESS', - payload: [{ firstName: 'Piotr', lastName: 'Witek' }], +const add = createCustomAction('CUSTOM', type => { + return (first: number, second: number) => ({ type, customProp1: first, customProp2: second }); }); -const failureResult = fetchUsers.failure(Error('Failure reason')); -expect(failureResult).toEqual({ - type: 'FETCH_USERS_FAILURE', - payload: Error('Failure reason'), -}); +add(1) // { type: "CUSTOM"; customProp1: number; customProp2: number; } ``` [⇧ back to top](#table-of-contents) -### createActionWithType +--- + +#### createAsyncAction -> create custom action-creator using constructor function with injected type argument +_Create an object containing three enhanced action-creators to simplify handling of async flows (e.g. network request - request/success/failure)._ ```ts -createActionWithType(type, constructorFunction): +createAsyncAction(requestType, successType, failureType) ``` Examples: -[> Advanced Usage Examples](src/create-action-with-type.spec.ts) +[> Advanced Usage Examples](src/create-async-action.spec.ts) ```ts -import { createActionWithType } from 'typesafe-actions'; - -it('with payload', () => { - const add = createActionWithType('WITH_MAPPED_PAYLOAD', type => { - return (amount: number) => ({ type, payload: amount }); - }); - const actual: { - type: 'WITH_MAPPED_PAYLOAD'; - payload: number; - } = add(1); - expect(actual).toEqual({ type: 'WITH_MAPPED_PAYLOAD', payload: 1 }); - }); - -it('with optional payload', () => { - const create = createActionWithType('WITH_OPTIONAL_PAYLOAD', type => { - return (id?: number) => ({ type, payload: id }); - }); - const actual1: { - type: 'WITH_OPTIONAL_PAYLOAD'; - payload: number | undefined; - } = create(); - expect(actual1).toEqual({ - type: 'WITH_OPTIONAL_PAYLOAD', - payload: undefined, - }); - const actual2: { - type: 'WITH_OPTIONAL_PAYLOAD'; - payload: number | undefined; - } = create(1); - expect(actual2).toEqual({ type: 'WITH_OPTIONAL_PAYLOAD', payload: 1 }); -}); +import { createAsyncAction } from 'typesafe-actions'; + +const fetchUsers = createAsyncAction( + 'FETCH_USERS_REQUEST', + 'FETCH_USERS_SUCCESS', + 'FETCH_USERS_FAILURE' +)(); + +fetchUsers.request(params); + +fetchUsers.success(response); + +fetchUsers.failure(err); ``` [⇧ back to top](#table-of-contents) --- -### getType - -> get the "type" property of a given action-creator -> contains properly narrowed literal type - +### Action-helpers -**NOTE**: ActionCreator type is generated from the `createAction` API. Simple [action](#action) creators throw a `RuntimeError` +#### getType +_Get the **type** property value (narrowed to literal type) of given enhanced action-creator._ ```ts -function getType(actionCreator: ActionCreator): T +getType(actionCreator) ``` [> Advanced Usage Examples](src/get-type.spec.ts) Examples: ```ts -const increment = createAction('INCREMENT'); -const type: 'INCREMENT' = getType(increment); -expect(type).toBe('INCREMENT'); +import { getType, createStandardAction } from 'typesafe-actions'; + +const add = createStandardAction('ADD')(); -// in reducer +// In switch reducer switch (action.type) { - case getType(increment): - return state + 1; + case getType(add): + // action type is { type: "ADD"; payload: number; } + return state + action.payload; default: return state; } + +// or with conditional statements +if (action.type === getType(add)) { + // action type is { type: "ADD"; payload: number; } +} ``` [⇧ back to top](#table-of-contents) --- -### isActionOf +#### isActionOf -> (curried assert function) check if action is an instance of given action-creator(s) -> it will narrow actions union to a specific action +_Check if action is an instance of given enhanced action-creator(s) +(it will narrow action type to a type of given action-creator(s))_ **NOTE**: ActionCreator type is generated from the `createAction` API. Simple [action](#action) creators throw a `RuntimeError` ```ts // can be used as a binary function -isActionOf(actionCreator: ActionCreator, action: any): action is T -isActionOf([actionCreator]: Array>, action: any): action is T -// or curried function -isActionOf(actionCreator: ActionCreator): (action: any) => action is T -isActionOf([actionCreator]: Array>): (action: any) => action is T +isActionOf(actionCreator, action) +// or as a curried function +isActionOf(actionCreator)(action) +// also accepts an array +isActionOf([actionCreator1, actionCreator2, ...actionCreatorN], action) +// with its curried equivalent +isActionOf([actionCreator1, actionCreator2, ...actionCreatorN])(action) ``` Examples: [> Advanced Usage Examples](src/is-action-of.spec.ts) ```ts -import { addTodo } from './todos-actions'; - -const addTodoToast: Epic = - (action$, store) => action$ - .filter(isActionOf(addTodo)) - .concatMap((action) => { // action is asserted as: { type: "ADD_TODO"; payload: string; } - const toast = `Added new todo: ${action.payload}`; - -// epics with multiple actions -import { addTodo, toggleTodo } from './todos-actions'; - -const logTodoAction: Epic = - (action$, store) => action$ - .filter(isActionOf([addTodo, toggleTodo])) - .concatMap((action) => { // action is asserted as: { type: "ADD_TODO"; payload: string; } | { type: "TOGGLE_TODO"; payload: string; } - const log = `Dispatched action: ${action.type}`; - -// conditionals where you need a type guard -import { addTodo } from './actions'; +import { addTodo, removeTodo } from './todos-actions'; +// Works with any filter type function (`Array.prototype.filter`, lodash, ramda, rxjs, etc.) +// - single action +[action1, action2, ...actionN] + .filter(isActionOf(addTodo)) // only actions with type `ADD` will pass + .map((action) => { + // action type is { type: "todos/ADD"; payload: Todo; } + ... + +// - multiple actions +[action1, action2, ...actionN] + .filter(isActionOf([addTodo, removeTodo])) // only actions with type `ADD` or 'REMOVE' will pass + .do((action) => { + // action type is { type: "todos/ADD"; payload: Todo; } | { type: "todos/REMOVE"; payload: Todo; } + ... + +// With conditional statements +// - single action if(isActionOf(addTodo, action)) { - operationThatNeedsPayload(action.payload) // action is asserted as: { type: "ADD_TODO"; payload: string; } + return iAcceptOnlyTodoType(action.payload); + // action type is { type: "todos/ADD"; payload: Todo; } +} +// - multiple actions +if(isActionOf([addTodo, removeTodo], action)) { + return iAcceptOnlyTodoType(action.payload); + // action type is { type: "todos/ADD"; payload: Todo; } | { type: "todos/REMOVE"; payload: Todo; } } ``` @@ -747,58 +689,53 @@ if(isActionOf(addTodo, action)) { --- -### isOfType +#### isOfType -> (curried assert function) check if action type is equal given type-constant -> it will narrow actions union to a specific action +_Check if action type property is equal given type-constant(s) +(it will narrow action type to a type of given action-creator(s))_ ```ts // can be used as a binary function -isOfType(type: T, action: any): action is Action -// or curried function -isOfType(type: T): (action: any) => action is T -// it also accepts an array of types to check against -isOfType(type: T[], action: any): action is Action -// and also works as curried function -isOfType(type: T[]): (action: any) => action is T +isOfType(type, action) +// or as curried function +isOfType(type)(action) +// also accepts an array +isOfType([type1, type2, ...typeN], action) +// with its curried equivalent +isOfType([type1, type2, ...typeN])(action) ``` Examples: [> Advanced Usage Examples](src/is-of-type.spec.ts) ```ts -import { ADD } from './todos-types'; - -const addTodoToast: Epic = - (action$, store, { toastService }) => action$ - .filter(isOfType(ADD)) - .do((action) => { - // action is narrowed as: { type: "todos/ADD"; payload: Todo; } - toastService.success(`Added new todo: ${action.payload}`); - }) - .ignoreElements(); -// Filter against array of actions import { ADD, REMOVE } from './todos-types'; -const addOrRemove: Epic = - (action$, store, { toastService }) => action$ - .filter(isOfType([ADD, REMOVE])) - .do((action) => { - // action is narrowed as: { type: "todos/ADD"; payload: Todo; } | { type: "todos/REMOVE"; payload: Todo; } - toastService.update(action.payload); - }) - .ignoreElements(); - -// conditionals where you need a type guard -import { ADD } from './todos-types'; - +// Works with any filter type function (`Array.prototype.filter`, lodash, ramda, rxjs, etc.) +// - single action +[action1, action2, ...actionN] + .filter(isOfType(ADD)) // only actions with type `ADD` will pass + .map((action) => { + // action type is { type: "todos/ADD"; payload: Todo; } + ... + +// - multiple actions +[action1, action2, ...actionN] + .filter(isOfType([ADD, REMOVE])) // only actions with type `ADD` or 'REMOVE' will pass + .do((action) => { + // action type is { type: "todos/ADD"; payload: Todo; } | { type: "todos/REMOVE"; payload: Todo; } + ... + +// With conditional statements +// - single action if(isOfType(ADD, action)) { - return functionThatAcceptsTodo(action.payload) // action: { type: "todos/ADD"; payload: Todo; } + return iAcceptOnlyTodoType(action.payload); + // action type is { type: "todos/ADD"; payload: Todo; } } -// or - +// - multiple actions if(isOfType([ADD, REMOVE], action)) { - return functionThatAcceptsTodo(action.payload) // action: { type: "todos/ADD"; payload: Todo; } | { type: "todos/REMOVE"; payload: Todo; } + return iAcceptOnlyTodoType(action.payload); + // action type is { type: "todos/ADD"; payload: Todo; } | { type: "todos/REMOVE"; payload: Todo; } } ``` @@ -807,10 +744,48 @@ if(isOfType([ADD, REMOVE], action)) { --- +### Action-factory + +#### action + +_Simple **action factory function** to simplify creation of type-safe actions._ + +**WARNING**: This approach will **NOT WORK** with **action-helpers** (such as `getType` and `isActionOf`) because it is creating simple actions. All the other creator functions will work because they are creating **enhanced action-creators**. + +```ts +action(type, payload?, meta?) +``` + +Examples: +[> Advanced Usage Examples](src/action.spec.ts) + +```ts +const increment = () => action('INCREMENT'); +// { type: 'INCREMENT'; } + +const createUser = (id: number, name: string) => + action('CREATE_USER', { id, name }); +// { type: 'CREATE_USER'; payload: { id: number; name: string }; } + +const getUsers = (params?: string) => + action('GET_USERS', undefined, params); +// { type: 'GET_USERS'; meta: string | undefined; } +``` + +**NOTICE**: Starting from TypeScript v3.4 you can achieve similar results using new `as const` operator. + +```ts +const increment = () => ({ type: 'INCREMENT' } as const); +``` + +[⇧ back to top](#table-of-contents) + +--- + ## Migration Guides ### v2.x.x to v3.x.x -v3.x.x API is backward compatible with v2.x.x. You'll only need to update typescript dependency to `> v3.2`. +v3.x.x API is backward compatible with v2.x.x. You'll only need to update typescript dependency to `> v3.1`. ### v1.x.x to v2.x.x > NOTE: `typesafe-actions@1.x.x` should be used with `utility-types@1.x.x` which contains `$call` utility (removed in `utility-types@2.x.x`) @@ -848,9 +823,9 @@ const getTodoStandardWithMap = createStandardAction('GET_TODO').map( ### Migrating from `redux-actions` -If you're using `redux-actions`, its `createAction` can be replaced with any of the above styles. Usage of its `createActions` function will need to be replaced with individual usages of `createAction`. The resulting hash of actions does not provide inference for the individual values. +If you're using `redux-actions` you can replace `createAction` with any of the above styles. Currently there is no equivalent of `createActions` function, so it will need to be replaced with individual usages of `createAction`. The resulting hash of actions does not provide inference for the individual values. -Additionally, if you're migrating from JS -> TS, you can swap out action creators with `typesafe-actions` and use them with `handleActions` from `redux-actions` in JS. This is because the action creators exposed by `typesafe-actions` provide the `toString` method used by `redux-actions` to route actions to the correct reducer. +Additionally, if migrating from JS -> TS, you can swap out `redux-actions` action-creators in your `handleActions` with action-creators from `typesafe-actions`. This works because the action creators exposed by `typesafe-actions` provide the `toString` method used by `redux-actions` to match actions to the correct reducer. [⇧ back to top](#table-of-contents) @@ -861,16 +836,14 @@ Additionally, if you're migrating from JS -> TS, you can swap out action creator Here you can find out a detailed comparison of `typesafe-actions` to other solutions. ### `redux-actions` -Lets compare the 3 most common action-creator variants (type only, with payload, with payload and meta) +Lets compare the 3 most common variants of action-creators (with type only, with payload and with payload + meta) -> tested with "@types/redux-actions": "2.2.3" +Note: tested with "@types/redux-actions": "2.2.3" -#### - type only (no payload) +#### - with type only (no payload) +##### redux-actions ```ts -/** - * redux-actions - */ const notify1 = createAction('NOTIFY'); // resulting type: // () => { @@ -879,28 +852,22 @@ const notify1 = createAction('NOTIFY'); // error: boolean | undefined; // } ``` +> with `redux-actions` you can notice the redundant nullable `payload` property and literal type of `type` property is lost (discrimination of union type would not be possible) -> with `redux-actions` you can notice the redundant nullable `payload` property and literal type of `type` property is lost (discrimination of union type would not be possible) (🐼 is really sad!) - +##### typesafe-actions ```ts -/** - * typesafe-actions - */ const notify1 = () => action('NOTIFY'); // resulting type: // () => { // type: "NOTIFY"; // } ``` - -> with `typesafe-actions` there is no excess nullable types, only the data that is really there, also the action "type" property is containing precise literal type +> with `typesafe-actions` there is no excess nullable types and no excess properties and the action "type" property is containing a literal type #### - with payload +##### redux-actions ```ts -/** - * redux-actions - */ const notify2 = createAction('NOTIFY', (username: string, message?: string) => ({ message: `${username}: ${message || 'Empty!'}`, @@ -913,13 +880,10 @@ const notify2 = createAction('NOTIFY', // error: boolean | undefined; // } ``` +> first the optional `message` parameter is lost, `username` semantic argument name is changed to some generic `t1`, `type` property is widened once again and `payload` is nullable because of broken inference -> first the optional `message` parameter is lost, `username` param name is changed to some generic `t1`, literal type of `type` property is lost again and `payload` is nullable because of broken inference - +##### typesafe-actions ```ts -/** - * typesafe-actions - */ const notify2 = (username: string, message?: string) => action( 'NOTIFY', { message: `${username}: ${message || 'Empty!'}` }, @@ -930,15 +894,12 @@ const notify2 = (username: string, message?: string) => action( // payload: { message: string; }; // } ``` - -> `typesafe-actions` still retain very precise resulting type +> `typesafe-actions` infer very precise resulting type, notice working optional parameters and semantic argument names are preserved which is really important for great intellisense experience #### - with payload and meta +##### redux-actions ```ts -/** - * redux-actions - */ const notify3 = createAction('NOTIFY', (username: string, message?: string) => ( { message: `${username}: ${message || 'Empty!'}` } @@ -955,9 +916,9 @@ const notify3 = createAction('NOTIFY', // error: boolean | undefined; // } ``` +> this time we got a completely broken arguments arity with no type-safety because of `any` type with all the earlier issues -> this time we got a complete loss of arguments arity with falling back to `any` type with all the remaining issues as before - +##### typesafe-actions ```ts /** * typesafe-actions @@ -974,8 +935,7 @@ const notify3 = (username: string, message?: string) => action( // meta: { username: string; message: string | undefined; }; // } ``` - -> `typesafe-actions` never fail to `any` type (🐼 is impressed by completely type-safe results) +> `typesafe-actions` never fail to `any` type, even with this advanced scenario all types are correct and provide complete type-safety and excellent developer experience [⇧ back to top](#table-of-contents) diff --git a/src/__snapshots__/create-action-with-type.spec.ts.snap b/src/__snapshots__/create-custom-action.spec.ts.snap similarity index 100% rename from src/__snapshots__/create-action-with-type.spec.ts.snap rename to src/__snapshots__/create-custom-action.spec.ts.snap diff --git a/src/create-async-action.ts b/src/create-async-action.ts index 8bd9f51..86984b3 100644 --- a/src/create-async-action.ts +++ b/src/create-async-action.ts @@ -1,5 +1,5 @@ import { StringType, Box, FsaMapBuilder, FsaBuilder } from './types'; -import { createActionWithType } from './create-action-with-type'; +import { createCustomAction } from './create-custom-action'; import { validateActionType } from './utils/utils'; export interface CreateAsyncAction< @@ -68,15 +68,15 @@ export function createAsyncAction< P3 > { return { - request: createActionWithType(requestType, type => (payload?: P1) => ({ + request: createCustomAction(requestType, type => (payload?: P1) => ({ type: requestType, payload, })) as FsaBuilder>, - success: createActionWithType(successType, type => (payload?: P2) => ({ + success: createCustomAction(successType, type => (payload?: P2) => ({ type: successType, payload, })) as FsaBuilder>, - failure: createActionWithType(failureType, type => (payload?: P3) => ({ + failure: createCustomAction(failureType, type => (payload?: P3) => ({ type: failureType, payload, })) as FsaBuilder>, @@ -89,15 +89,15 @@ export function createAsyncAction< // failureMapper: (a?: A3) => P3 // ): AsyncActionWithMappers { // return { - // request: createActionWithType(requestType, type => (payload?: A1) => ({ + // request: createCustomAction(requestType, type => (payload?: A1) => ({ // type, // payload: requestMapper != null ? requestMapper(payload) : undefined, // })) as MapBuilder, B>, - // success: createActionWithType(successType, type => (payload?: A2) => ({ + // success: createCustomAction(successType, type => (payload?: A2) => ({ // type, // payload: successMapper != null ? successMapper(payload) : undefined, // })) as MapBuilder, B>, - // failure: createActionWithType(failureType, type => (payload?: A3) => ({ + // failure: createCustomAction(failureType, type => (payload?: A3) => ({ // type, // payload: failureMapper != null ? failureMapper(payload) : undefined, // })) as MapBuilder, B>, diff --git a/src/create-action-with-type.spec.snap.ts b/src/create-custom-action.spec.snap.ts similarity index 79% rename from src/create-action-with-type.spec.snap.ts rename to src/create-custom-action.spec.snap.ts index 6cc7a21..0a2b457 100644 --- a/src/create-action-with-type.spec.snap.ts +++ b/src/create-custom-action.spec.snap.ts @@ -1,10 +1,10 @@ import * as Types from './types'; -import { createActionWithType } from './create-action-with-type'; +import { createCustomAction } from './create-custom-action'; -describe('createActionWithType', () => { +describe('createCustomAction', () => { it('with type only using symbol', () => { const INCREMENT = Symbol(1); - const increment = createActionWithType(INCREMENT, type => () => ({ type })); + const increment = createCustomAction(INCREMENT, type => () => ({ type })); const actual = increment(); // @dts-jest:pass:snap -> { type: unique symbol; } actual; @@ -12,7 +12,7 @@ describe('createActionWithType', () => { }); it('with type only', () => { - const increment = createActionWithType('WITH_TYPE_ONLY'); + const increment = createCustomAction('WITH_TYPE_ONLY'); const actual: { type: 'WITH_TYPE_ONLY'; } = increment(); @@ -20,7 +20,7 @@ describe('createActionWithType', () => { }); it('with payload', () => { - const add = createActionWithType('WITH_PAYLOAD', type => { + const add = createCustomAction('WITH_PAYLOAD', type => { return (amount: number) => ({ type, payload: amount }); }); const actual: { @@ -31,7 +31,7 @@ describe('createActionWithType', () => { }); it('with optional payload', () => { - const create = createActionWithType('WITH_OPTIONAL_PAYLOAD', type => { + const create = createCustomAction('WITH_OPTIONAL_PAYLOAD', type => { return (id?: number) => ({ type, payload: id }); }); const actual1: { @@ -50,7 +50,7 @@ describe('createActionWithType', () => { }); it('with payload and meta', () => { - const showNotification = createActionWithType( + const showNotification = createCustomAction( 'SHOW_NOTIFICATION', type => (message: string, scope: string) => ({ type, diff --git a/src/create-action-with-type.spec.ts b/src/create-custom-action.spec.ts similarity index 78% rename from src/create-action-with-type.spec.ts rename to src/create-custom-action.spec.ts index 4f4a465..7363b7f 100644 --- a/src/create-action-with-type.spec.ts +++ b/src/create-custom-action.spec.ts @@ -1,10 +1,10 @@ import * as Types from './types'; -import { createActionWithType } from './create-action-with-type'; +import { createCustomAction } from './create-custom-action'; -describe('createActionWithType', () => { +describe('createCustomAction', () => { it('with type only using symbol', () => { const INCREMENT = Symbol(1); - const increment = createActionWithType(INCREMENT, type => () => ({ type })); + const increment = createCustomAction(INCREMENT, type => () => ({ type })); const actual = increment(); // @dts-jest:pass:snap actual; @@ -12,7 +12,7 @@ describe('createActionWithType', () => { }); it('with type only', () => { - const increment = createActionWithType('WITH_TYPE_ONLY'); + const increment = createCustomAction('WITH_TYPE_ONLY'); const actual: { type: 'WITH_TYPE_ONLY'; } = increment(); @@ -20,7 +20,7 @@ describe('createActionWithType', () => { }); it('with payload', () => { - const add = createActionWithType('WITH_PAYLOAD', type => { + const add = createCustomAction('WITH_PAYLOAD', type => { return (amount: number) => ({ type, payload: amount }); }); const actual: { @@ -31,7 +31,7 @@ describe('createActionWithType', () => { }); it('with optional payload', () => { - const create = createActionWithType('WITH_OPTIONAL_PAYLOAD', type => { + const create = createCustomAction('WITH_OPTIONAL_PAYLOAD', type => { return (id?: number) => ({ type, payload: id }); }); const actual1: { @@ -50,7 +50,7 @@ describe('createActionWithType', () => { }); it('with payload and meta', () => { - const showNotification = createActionWithType( + const showNotification = createCustomAction( 'SHOW_NOTIFICATION', type => (message: string, scope: string) => ({ type, diff --git a/src/create-action-with-type.ts b/src/create-custom-action.ts similarity index 93% rename from src/create-action-with-type.ts rename to src/create-custom-action.ts index cc2833d..802e6bf 100644 --- a/src/create-action-with-type.ts +++ b/src/create-custom-action.ts @@ -3,7 +3,7 @@ import { ActionCreator, StringOrSymbol } from './types'; /** * @description create custom action-creator using constructor function with injected type argument */ -export function createActionWithType< +export function createCustomAction< T extends StringOrSymbol, AC extends ActionCreator = () => { type: T } >(type: T, actionCreatorHandler?: (type: T) => AC): AC { diff --git a/src/create-standard-action.ts b/src/create-standard-action.ts index 710ff4e..7ca429a 100644 --- a/src/create-standard-action.ts +++ b/src/create-standard-action.ts @@ -1,5 +1,5 @@ import { StringType, Box, FsaBuilder, FsaMapBuilder } from './types'; -import { createActionWithType } from './create-action-with-type'; +import { createCustomAction } from './create-custom-action'; import { validateActionType } from './utils/utils'; export interface CreateStandardAction { @@ -18,7 +18,7 @@ export function createStandardAction( validateActionType(actionType); function constructor(): FsaBuilder, Box> { - return createActionWithType(actionType, type => (payload: P, meta: M) => ({ + return createCustomAction(actionType, type => (payload: P, meta: M) => ({ type, payload, meta, @@ -28,7 +28,7 @@ export function createStandardAction( function map( fn: (payload: P, meta: M) => R ): FsaMapBuilder, Box

, Box> { - return createActionWithType(actionType, type => (payload: P, meta: M) => + return createCustomAction(actionType, type => (payload: P, meta: M) => Object.assign(fn(payload, meta), { type }) ) as FsaMapBuilder, Box

, Box>; } diff --git a/src/index.ts b/src/index.ts index 07cf985..0587d88 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ export { action } from './action'; export { createAction } from './create-action'; export { createStandardAction } from './create-standard-action'; +export { createCustomAction } from './create-custom-action'; export { createAsyncAction } from './create-async-action'; // guards export { ActionType, StateType } from './types';