Skip to content

Commit

Permalink
fix: type for plain action creator. (the-dr-lazy#144)
Browse files Browse the repository at this point in the history
* fix: type for plain action creator. Fixes the-dr-lazy#143

* fix: more type tests for createActionCreator

* fix: adjustments after code review

Co-authored-by: Mohammad Hasani <[email protected]>
  • Loading branch information
SantoJambit and the-dr-lazy committed Oct 1, 2020
1 parent 9925130 commit f228b81
Show file tree
Hide file tree
Showing 4 changed files with 277 additions and 17 deletions.
122 changes: 115 additions & 7 deletions src/__tests__/__snapshots__/create-action-creator.dts.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -1,35 +1,143 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`createActionCreator const todoAdd = createActionCreator(
'[Todo] add',
// tslint:disable-next-line:no-inferrable-types
resolve => (name: string, completed: boolean = false) =>
resolve({ name, completed })
) (type) should match snapshot 1`] = `"any"`;

exports[`createActionCreator const todoAdd2 = createActionCreator(
'[Todo] add',
// tslint:disable-next-line:no-inferrable-types
resolve => (name: string, completed: boolean = false) =>
resolve({ name, completed }, 'Meta data of all todos')
) (type) should match snapshot 1`] = `"any"`;

exports[`createActionCreator const todoFetchRejected = createActionCreator('[Todo] fetch rejected', resolve => (error: Error) =>
resolve(error)
) (type) should match snapshot 1`] = `"any"`;

exports[`createActionCreator const todoFetchRejected2 = createActionCreator(
'[Todo] fetch rejected',
resolve => (error: Error, meta?: { status: number }) => resolve(error, meta)
) (type) should match snapshot 1`] = `"any"`;

exports[`createActionCreator const todoGeneric = createActionCreator('[Todo] generic', resolve => <Name>(name: Name) =>
resolve(name)
) (type) should match snapshot 1`] = `"any"`;
exports[`createActionCreator const todoGeneric2 = createActionCreator(
'[Todo] generic',
resolve => <Name, Meta>(name: Name, meta: Meta) => resolve(name, meta)
) (type) should match snapshot 1`] = `"any"`;
exports[`createActionCreator const todoTruncate = createActionCreator('[Todo] truncate') (type) should match snapshot 1`] = `"any"`;
exports[`createActionCreator const todoVarArgs = createActionCreator(
'[Todo] var-args',
resolve => (...names: string[]) => resolve(names)
) (type) should match snapshot 1`] = `"any"`;
exports[`createActionCreator createActionCreator(
'[Todo] add',
// tslint:disable-next-line:no-inferrable-types
resolve => (name: string, completed: boolean = false) =>
resolve({ name, completed })
) (type) should match snapshot 1`] = `"((name: string, completed?: boolean) => { type: \\"[Todo] add\\"; payload: { name: string; completed: boolean; }; }) & { type: \\"[Todo] add\\"; toString(): \\"[Todo] add\\"; }"`;
) (type) should match snapshot 1`] = `"ExactActionCreator<\\"[Todo] add\\", (name: string, completed?: boolean) => { type: \\"[Todo] add\\"; payload: { name: string; completed: boolean; }; }>"`;
exports[`createActionCreator createActionCreator(
'[Todo] add',
// tslint:disable-next-line:no-inferrable-types
resolve => (name: string, completed: boolean = false) =>
resolve({ name, completed }, 'Meta data of all todos')
) (type) should match snapshot 1`] = `"((name: string, completed?: boolean) => { type: \\"[Todo] add\\"; payload: { name: string; completed: boolean; }; meta: string; }) & { type: \\"[Todo] add\\"; toString(): \\"[Todo] add\\"; }"`;
) (type) should match snapshot 1`] = `"ExactActionCreator<\\"[Todo] add\\", (name: string, completed?: boolean) => { type: \\"[Todo] add\\"; payload: { name: string; completed: boolean; }; meta: string; }>"`;
exports[`createActionCreator createActionCreator(
'[Todo] fetch rejected',
resolve => (error: Error, meta?: { status: number }) => resolve(error, meta)
) (type) should match snapshot 1`] = `"((error: Error, meta?: { status: number; } | undefined) => { type: \\"[Todo] fetch rejected\\"; payload: Error; meta: { status: number; }; error: true; }) & { type: \\"[Todo] fetch rejected\\"; toString(): \\"[Todo] fetch rejected\\"; }"`;
) (type) should match snapshot 1`] = `"ExactActionCreator<\\"[Todo] fetch rejected\\", (error: Error, meta?: { status: number; } | undefined) => { type: \\"[Todo] fetch rejected\\"; payload: Error; meta: { status: number; }; error: true; }>"`;
exports[`createActionCreator createActionCreator(
'[Todo] generic',
resolve => <Name, Meta>(name: Name, meta: Meta) => resolve(name, meta)
) (type) should match snapshot 1`] = `"(<Name, Meta>(name: Name, meta: Meta) => Action<\\"[Todo] generic\\", Name, Meta>) & { type: \\"[Todo] generic\\"; toString(): \\"[Todo] generic\\"; }"`;
) (type) should match snapshot 1`] = `"ExactActionCreator<\\"[Todo] generic\\", <Name, Meta>(name: Name, meta: Meta) => Action<\\"[Todo] generic\\", Name, Meta>>"`;
exports[`createActionCreator createActionCreator(
'[Todo] var-args',
resolve => (...names: string[]) => resolve(names)
) (type) should match snapshot 1`] = `"ExactActionCreator<\\"[Todo] var-args\\", (...names: string[]) => { type: \\"[Todo] var-args\\"; payload: string[]; }>"`;
exports[`createActionCreator createActionCreator('[Todo] fetch rejected', resolve => (error: Error) =>
resolve(error)
) (type) should match snapshot 1`] = `"((error: Error) => { type: \\"[Todo] fetch rejected\\"; payload: Error; error: true; }) & { type: \\"[Todo] fetch rejected\\"; toString(): \\"[Todo] fetch rejected\\"; }"`;
) (type) should match snapshot 1`] = `"ExactActionCreator<\\"[Todo] fetch rejected\\", (error: Error) => { type: \\"[Todo] fetch rejected\\"; payload: Error; error: true; }>"`;
exports[`createActionCreator createActionCreator('[Todo] generic', resolve => <Name>(name: Name) =>
resolve(name)
) (type) should match snapshot 1`] = `"(<Name>(name: Name) => Action<\\"[Todo] generic\\", Name, undefined>) & { type: \\"[Todo] generic\\"; toString(): \\"[Todo] generic\\"; }"`;
) (type) should match snapshot 1`] = `"ExactActionCreator<\\"[Todo] generic\\", <Name>(name: Name) => Action<\\"[Todo] generic\\", Name, undefined>>"`;
exports[`createActionCreator createActionCreator('[Todo] truncate') (type) should match snapshot 1`] = `"ExactActionCreator<\\"[Todo] truncate\\", () => { type: \\"[Todo] truncate\\"; }>"`;
exports[`createActionCreator todoAdd('buy presents') (type) should match snapshot 1`] = `"{ type: \\"[Todo] add\\"; payload: { name: string; completed: boolean; }; }"`;
exports[`createActionCreator todoAdd('buy presents', true) (type) should match snapshot 1`] = `"{ type: \\"[Todo] add\\"; payload: { name: string; completed: boolean; }; }"`;
exports[`createActionCreator todoAdd('buy presents', true, 42) (type) should match snapshot 1`] = `"Expected 1-2 arguments, but got 3."`;
exports[`createActionCreator todoAdd() (type) should match snapshot 1`] = `"Expected 1-2 arguments, but got 0."`;
exports[`createActionCreator todoAdd(false) (type) should match snapshot 1`] = `"Argument of type 'false' is not assignable to parameter of type 'string'."`;
exports[`createActionCreator todoAdd2('buy presents') (type) should match snapshot 1`] = `"{ type: \\"[Todo] add\\"; payload: { name: string; completed: boolean; }; meta: string; }"`;
exports[`createActionCreator todoAdd2('buy presents', true) (type) should match snapshot 1`] = `"{ type: \\"[Todo] add\\"; payload: { name: string; completed: boolean; }; meta: string; }"`;
exports[`createActionCreator todoAdd2() (type) should match snapshot 1`] = `"Expected 1-2 arguments, but got 0."`;
exports[`createActionCreator todoFetchRejected() (type) should match snapshot 1`] = `"Expected 1 arguments, but got 0."`;
exports[`createActionCreator todoFetchRejected(new Error('')) (type) should match snapshot 1`] = `"{ type: \\"[Todo] fetch rejected\\"; payload: Error; error: true; }"`;
exports[`createActionCreator todoFetchRejected2() (type) should match snapshot 1`] = `"Expected 1-2 arguments, but got 0."`;
exports[`createActionCreator todoFetchRejected2(new Error('')) (type) should match snapshot 1`] = `"{ type: \\"[Todo] fetch rejected\\"; payload: Error; meta: { status: number; }; error: true; }"`;
exports[`createActionCreator todoFetchRejected2(new Error(''), { status: '' }) (type) should match snapshot 1`] = `
"Argument of type '{ status: string; }' is not assignable to parameter of type '{ status: number; }'.
Types of property 'status' are incompatible.
Type 'string' is not assignable to type 'number'."
`;
exports[`createActionCreator todoFetchRejected2(new Error(''), { status: 200 }) (type) should match snapshot 1`] = `"{ type: \\"[Todo] fetch rejected\\"; payload: Error; meta: { status: number; }; error: true; }"`;
exports[`createActionCreator todoGeneric('buy presents') (type) should match snapshot 1`] = `"{ type: \\"[Todo] generic\\"; payload: string; }"`;
exports[`createActionCreator todoGeneric('buy presents', true) (type) should match snapshot 1`] = `"Expected 1 arguments, but got 2."`;
exports[`createActionCreator todoGeneric() (type) should match snapshot 1`] = `"Expected 1 arguments, but got 0."`;
exports[`createActionCreator todoGeneric(false) (type) should match snapshot 1`] = `"{ type: \\"[Todo] generic\\"; payload: false; } | { type: \\"[Todo] generic\\"; payload: true; }"`;
exports[`createActionCreator todoGeneric2('The answer', 42) (type) should match snapshot 1`] = `"{ type: \\"[Todo] generic\\"; payload: string; meta: number; }"`;
exports[`createActionCreator todoGeneric2('The answer', 42, 'Zaphod') (type) should match snapshot 1`] = `"Expected 2 arguments, but got 3."`;
exports[`createActionCreator todoGeneric2() (type) should match snapshot 1`] = `"Expected 2 arguments, but got 0."`;
exports[`createActionCreator todoGeneric2(42) (type) should match snapshot 1`] = `"Expected 2 arguments, but got 1."`;
exports[`createActionCreator todoGeneric2(42, 'The answer') (type) should match snapshot 1`] = `"{ type: \\"[Todo] generic\\"; payload: number; meta: string; }"`;
exports[`createActionCreator todoTruncate('') (type) should match snapshot 1`] = `"Expected 0 arguments, but got 1."`;
exports[`createActionCreator todoTruncate() (type) should match snapshot 1`] = `"{ type: \\"[Todo] truncate\\"; }"`;
exports[`createActionCreator todoVarArgs('Attend Christmas Party', 'Wish everyone a Merry Christmas') (type) should match snapshot 1`] = `"{ type: \\"[Todo] var-args\\"; payload: string[]; }"`;
exports[`createActionCreator todoVarArgs('Dress Up like Santa', 'Ignore numbers 1-3', 'Steal Christmas!') (type) should match snapshot 1`] = `"{ type: \\"[Todo] var-args\\"; payload: string[]; }"`;
exports[`createActionCreator todoVarArgs('Make Cookies') (type) should match snapshot 1`] = `"{ type: \\"[Todo] var-args\\"; payload: string[]; }"`;
exports[`createActionCreator todoVarArgs() (type) should match snapshot 1`] = `"{ type: \\"[Todo] var-args\\"; payload: string[]; }"`;
exports[`createActionCreator createActionCreator('[Todo] truncate') (type) should match snapshot 1`] = `"(<_T>(...args: any[]) => { type: \\"[Todo] truncate\\"; }) & { type: \\"[Todo] truncate\\"; toString(): \\"[Todo] truncate\\"; }"`;
exports[`createActionCreator todoVarArgs(false) (type) should match snapshot 1`] = `"Argument of type 'false' is not assignable to parameter of type 'string'."`;
143 changes: 143 additions & 0 deletions src/__tests__/create-action-creator.dts.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,55 @@ import { createActionCreator } from '../create-action-creator'
// @dts-jest:pass:snap
createActionCreator('[Todo] truncate')

// @dts-jest:pass:snap
const todoTruncate = createActionCreator('[Todo] truncate')

// @dts-jest:fail:snap
todoTruncate('')

// @dts-jest:pass:snap
todoTruncate()

// @dts-jest:pass:snap
createActionCreator('[Todo] fetch rejected', resolve => (error: Error) =>
resolve(error)
)

// @dts-jest:pass:snap
const todoFetchRejected = createActionCreator('[Todo] fetch rejected', resolve => (error: Error) =>
resolve(error)
)

// @dts-jest:fail:snap
todoFetchRejected()

// @dts-jest:pass:snap
todoFetchRejected(new Error(''))

// @dts-jest:pass:snap
createActionCreator(
'[Todo] fetch rejected',
resolve => (error: Error, meta?: { status: number }) => resolve(error, meta)
)

// @dts-jest:pass:snap
const todoFetchRejected2 = createActionCreator(
'[Todo] fetch rejected',
resolve => (error: Error, meta?: { status: number }) => resolve(error, meta)
)

// @dts-jest:fail:snap
todoFetchRejected2()

// @dts-jest:pass:snap
todoFetchRejected2(new Error(''))

// @dts-jest:pass:snap
todoFetchRejected2(new Error(''), { status: 200 })

// @dts-jest:fail:snap
todoFetchRejected2(new Error(''), { status: '' })

// @dts-jest:pass:snap
createActionCreator(
'[Todo] add',
Expand All @@ -24,6 +62,29 @@ createActionCreator(
resolve({ name, completed })
)

// @dts-jest:pass:snap
const todoAdd = createActionCreator(
'[Todo] add',
// tslint:disable-next-line:no-inferrable-types
resolve => (name: string, completed: boolean = false) =>
resolve({ name, completed })
)

// @dts-jest:fail:snap
todoAdd()

// @dts-jest:fail:snap
todoAdd(false);

// @dts-jest:pass:snap
todoAdd('buy presents');

// @dts-jest:pass:snap
todoAdd('buy presents', true);

// @dts-jest:fail:snap
todoAdd('buy presents', true, 42);

// @dts-jest:pass:snap
createActionCreator(
'[Todo] add',
Expand All @@ -32,13 +93,95 @@ createActionCreator(
resolve({ name, completed }, 'Meta data of all todos')
)

// @dts-jest:pass:snap
const todoAdd2 = createActionCreator(
'[Todo] add',
// tslint:disable-next-line:no-inferrable-types
resolve => (name: string, completed: boolean = false) =>
resolve({ name, completed }, 'Meta data of all todos')
)

// @dts-jest:fail:snap
todoAdd2()

// @dts-jest:pass:snap
todoAdd2('buy presents');

// @dts-jest:pass:snap
todoAdd2('buy presents', true);

// @dts-jest:pass:snap
createActionCreator('[Todo] generic', resolve => <Name>(name: Name) =>
resolve(name)
)

// @dts-jest:pass:snap
const todoGeneric = createActionCreator('[Todo] generic', resolve => <Name>(name: Name) =>
resolve(name)
)

// @dts-jest:fail:snap
todoGeneric()

// @dts-jest:pass:snap
todoGeneric('buy presents');

// @dts-jest:pass:snap
todoGeneric(false);

// @dts-jest:fail:snap
todoGeneric('buy presents', true);

// @dts-jest:pass:snap
createActionCreator(
'[Todo] generic',
resolve => <Name, Meta>(name: Name, meta: Meta) => resolve(name, meta)
)

// @dts-jest:pass:snap
const todoGeneric2 = createActionCreator(
'[Todo] generic',
resolve => <Name, Meta>(name: Name, meta: Meta) => resolve(name, meta)
)

// @dts-jest:fail:snap
todoGeneric2()

// @dts-jest:fail:snap
todoGeneric2(42);

// @dts-jest:pass:snap
todoGeneric2(42, 'The answer');

// @dts-jest:pass:snap
todoGeneric2('The answer', 42);

// @dts-jest:fail:snap
todoGeneric2('The answer', 42, 'Zaphod');

// @dts-jest:pass:snap
createActionCreator(
'[Todo] var-args',
resolve => (...names: string[]) => resolve(names)
)

// @dts-jest:pass:snap
const todoVarArgs = createActionCreator(
'[Todo] var-args',
resolve => (...names: string[]) => resolve(names)
)

// @dts-jest:fail:snap
todoVarArgs(false)

// @dts-jest:pass:snap
todoVarArgs()

// @dts-jest:pass:snap
todoVarArgs('Make Cookies')

// @dts-jest:pass:snap
todoVarArgs('Attend Christmas Party', 'Wish everyone a Merry Christmas')

// @dts-jest:pass:snap
todoVarArgs('Dress Up like Santa', 'Ignore numbers 1-3', 'Steal Christmas!')
2 changes: 1 addition & 1 deletion src/__tests__/is-of-type.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ describe('isOfType', () => {
${[increment(), decrement(), reset(0)]} | ${increment()} | ${true}
${[increment(), reset(0)]} | ${decrement()} | ${false}
${[decrement(), reset(1)]} | ${reset(0)} | ${true}
${increment} | ${increment(0)} | ${true}
${increment} | ${increment()} | ${true}
${decrement} | ${increment()} | ${false}
${reset} | ${reset(0)} | ${true}
${[increment, decrement, reset]} | ${increment()} | ${true}
Expand Down
27 changes: 18 additions & 9 deletions src/create-action-creator.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
import { createAction, Action, AnyAction } from './create-action'

export type ExactActionCreator<TType extends string, TCallable extends <_T>(...args: any[]) => Action<TType>> = TCallable & {
type: TType extends AnyAction ? TType['type'] : TType
toString(): TType extends AnyAction ? TType['type'] : TType
}

export type ActionCreator<T extends AnyAction | string> = {
(...args: any[]): T extends string ? Action<T> : T
type: T extends AnyAction ? T['type'] : T
toString(): T extends AnyAction ? T['type'] : T
}

export type Executor<TType extends string, TCallable extends <_T>(...args: any[]) => Action<TType>> = (
resolve: <Payload = undefined, Meta = undefined>(
payload?: Payload,
meta?: Meta
) => Action<TType, Payload, Meta>
) => TCallable;

/**
* Flux standard action creator factory
* @example
Expand All @@ -32,18 +44,15 @@ export type ActionCreator<T extends AnyAction | string> = {
* )
*
*/
export function createActionCreator<TType extends string>(type: TType): ExactActionCreator<TType, () => Action<TType>>;
export function createActionCreator<
TType extends string,
TCallable extends <_T>(...args: any[]) => Action<TType>
>(type: TType, executor: Executor<TType, TCallable>): ExactActionCreator<TType, TCallable>;
export function createActionCreator<
TType extends string,
TCallable extends <_T>(...args: any[]) => Action<TType>
>(
type: TType,
executor: (
resolve: <Payload = undefined, Meta = undefined>(
payload?: Payload,
meta?: Meta
) => Action<TType, Payload, Meta>
) => TCallable = resolve => (() => resolve()) as TCallable
) {
>(type: TType, executor: Executor<TType, TCallable> = resolve => (() => resolve()) as TCallable) {
const callable = executor((payload, meta) =>
createAction(type, payload!, meta!)
)
Expand Down

0 comments on commit f228b81

Please sign in to comment.