Skip to content

Commit 5ed3c3d

Browse files
feat(store): provide better TS errors for action creator props (#3060)
Closes #2892 BREAKING CHANGES: Types for props outside an action creator is more strictly checked BEFORE: Usage of `props` outside of an action creator with invalid types was allowed AFTER: Usage of `props` outside of an action creator now breaks for invalid types
1 parent 2b53495 commit 5ed3c3d

File tree

4 files changed

+107
-12
lines changed

4 files changed

+107
-12
lines changed

modules/store/spec/types/action_creator.spec.ts

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,23 +39,79 @@ describe('createAction()', () => {
3939
expectSnippet(`
4040
const foo = createAction('FOO', props<{ type: number }>());
4141
`).toFail(
42-
/ Type 'ActionCreatorProps<\{ type: number; \}>' is not assignable to type '"type property is not allowed in action creators"'/
42+
/Type '{ type: number; }' does not satisfy the constraint '"action creator props cannot have a property named `type`"'/
4343
);
4444
});
4545

4646
it('should not allow arrays', () => {
4747
expectSnippet(`
4848
const foo = createAction('FOO', props<[]>());
4949
`).toFail(
50-
/Type 'ActionCreatorProps<\[\]>' is not assignable to type '"arrays are not allowed in action creators"'/
50+
/Type '\[]' does not satisfy the constraint '"action creator props cannot be an array"'/
5151
);
5252
});
5353

5454
it('should not allow empty objects', () => {
5555
expectSnippet(`
5656
const foo = createAction('FOO', props<{}>());
5757
`).toFail(
58-
/Type 'ActionCreatorProps<\{\}>' is not assignable to type '"empty objects are not allowed in action creators"'/
58+
/Type '{}' does not satisfy the constraint '"action creator props cannot be an empty object"'/
59+
);
60+
});
61+
62+
it('should not allow strings', () => {
63+
expectSnippet(`
64+
const foo = createAction('FOO', props<string>());
65+
`).toFail(
66+
/Type 'string' does not satisfy the constraint '"action creator props cannot be a primitive value"'/
67+
);
68+
});
69+
70+
it('should not allow numbers', () => {
71+
expectSnippet(`
72+
const foo = createAction('FOO', props<number>());
73+
`).toFail(
74+
/Type 'number' does not satisfy the constraint '"action creator props cannot be a primitive value"'/
75+
);
76+
});
77+
78+
it('should not allow bigints', () => {
79+
expectSnippet(`
80+
const foo = createAction('FOO', props<bigint>());
81+
`).toFail(
82+
/Type 'bigint' does not satisfy the constraint '"action creator props cannot be a primitive value"'/
83+
);
84+
});
85+
86+
it('should not allow booleans', () => {
87+
expectSnippet(`
88+
const foo = createAction('FOO', props<boolean>());
89+
`).toFail(
90+
/Type 'boolean' does not satisfy the constraint '"action creator props cannot be a primitive value"'/
91+
);
92+
});
93+
94+
it('should not allow symbols', () => {
95+
expectSnippet(`
96+
const foo = createAction('FOO', props<symbol>());
97+
`).toFail(
98+
/Type 'symbol' does not satisfy the constraint '"action creator props cannot be a primitive value"'/
99+
);
100+
});
101+
102+
it('should not allow null', () => {
103+
expectSnippet(`
104+
const foo = createAction('FOO', props<null>());
105+
`).toFail(
106+
/Type 'ActionCreatorProps<null>' is not assignable to type '"action creator cannot return an array"'/
107+
);
108+
});
109+
110+
it('should not allow undefined', () => {
111+
expectSnippet(`
112+
const foo = createAction('FOO', props<undefined>());
113+
`).toFail(
114+
/Type 'ActionCreatorProps<undefined>' is not assignable to type '"action creator cannot return an array"'/
59115
);
60116
});
61117
});
@@ -88,7 +144,7 @@ describe('createAction()', () => {
88144
expectSnippet(`
89145
const foo = createAction('FOO', (type: string) => ({type}));
90146
`).toFail(
91-
/Type '\{ type: string; \}' is not assignable to type '"type property is not allowed in action creators"'/
147+
/Type '\{ type: string; \}' is not assignable to type '"action creator cannot return an object with a property named `type`"'/
92148
);
93149
});
94150

@@ -102,7 +158,7 @@ describe('createAction()', () => {
102158
expectSnippet(`
103159
const foo = createAction('FOO', () => [ ]);
104160
`).toFail(
105-
/Type 'any\[\]' is not assignable to type '"arrays are not allowed in action creators"'/
161+
/Type 'any\[\]' is not assignable to type '"action creator cannot return an array"'/
106162
);
107163
});
108164
});

modules/store/src/action_creator.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
FunctionWithParametersType,
66
NotAllowedCheck,
77
ActionCreatorProps,
8+
NotAllowedInPropsCheck,
89
} from './models';
910
import { REGISTERED_ACTION_TYPES } from './globals';
1011

@@ -124,7 +125,10 @@ export function createAction<T extends string, C extends Creator>(
124125
}
125126
}
126127

127-
export function props<P extends object>(): ActionCreatorProps<P> {
128+
export function props<
129+
P extends SafeProps,
130+
SafeProps = NotAllowedInPropsCheck<P>
131+
>(): ActionCreatorProps<P> {
128132
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/naming-convention
129133
return { _as: 'props', _p: undefined! };
130134
}

modules/store/src/feature_creator_models.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { MemoizedSelector } from './selector';
2+
import { Primitive } from './models';
23

34
// Generating documentation for `createFeature` function is solved by moving types that use
45
// template literal types (`FeatureSelector` and `NestedSelectors`) from `feature_creator.ts`.
@@ -28,5 +29,3 @@ export type NestedSelectors<
2829
FeatureState[K]
2930
>;
3031
};
31-
32-
type Primitive = string | number | bigint | boolean | null | undefined;

modules/store/src/models.ts

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,18 +56,33 @@ export type SelectorWithProps<State, Props, Result> = (
5656
props: Props
5757
) => Result;
5858

59-
export const arraysAreNotAllowedMsg =
60-
'arrays are not allowed in action creators';
59+
export const arraysAreNotAllowedMsg = 'action creator cannot return an array';
6160
type ArraysAreNotAllowed = typeof arraysAreNotAllowedMsg;
6261

6362
export const typePropertyIsNotAllowedMsg =
64-
'type property is not allowed in action creators';
63+
'action creator cannot return an object with a property named `type`';
6564
type TypePropertyIsNotAllowed = typeof typePropertyIsNotAllowedMsg;
6665

6766
export const emptyObjectsAreNotAllowedMsg =
68-
'empty objects are not allowed in action creators';
67+
'action creator cannot return an empty object';
6968
type EmptyObjectsAreNotAllowed = typeof emptyObjectsAreNotAllowedMsg;
7069

70+
export const arraysAreNotAllowedInProps =
71+
'action creator props cannot be an array';
72+
type ArraysAreNotAllowedInProps = typeof arraysAreNotAllowedInProps;
73+
74+
export const typePropertyIsNotAllowedInProps =
75+
'action creator props cannot have a property named `type`';
76+
type TypePropertyIsNotAllowedInProps = typeof typePropertyIsNotAllowedInProps;
77+
78+
export const emptyObjectsAreNotAllowedInProps =
79+
'action creator props cannot be an empty object';
80+
type EmptyObjectsAreNotAllowedInProps = typeof emptyObjectsAreNotAllowedInProps;
81+
82+
export const primitivesAreNotAllowedInProps =
83+
'action creator props cannot be a primitive value';
84+
type PrimitivesAreNotAllowedInProps = typeof primitivesAreNotAllowedInProps;
85+
7186
export type FunctionIsNotAllowed<
7287
T,
7388
ErrorMessage extends string
@@ -80,6 +95,15 @@ export type Creator<
8095
R extends object = object
8196
> = FunctionWithParametersType<P, R>;
8297

98+
export type Primitive =
99+
| string
100+
| number
101+
| bigint
102+
| boolean
103+
| symbol
104+
| null
105+
| undefined;
106+
83107
export type NotAllowedCheck<T extends object> = T extends any[]
84108
? ArraysAreNotAllowed
85109
: T extends { type: any }
@@ -88,6 +112,18 @@ export type NotAllowedCheck<T extends object> = T extends any[]
88112
? EmptyObjectsAreNotAllowed
89113
: unknown;
90114

115+
export type NotAllowedInPropsCheck<T> = T extends object
116+
? T extends any[]
117+
? ArraysAreNotAllowedInProps
118+
: T extends { type: any }
119+
? TypePropertyIsNotAllowedInProps
120+
: keyof T extends never
121+
? EmptyObjectsAreNotAllowedInProps
122+
: unknown
123+
: T extends Primitive
124+
? PrimitivesAreNotAllowedInProps
125+
: never;
126+
91127
/**
92128
* See `Creator`.
93129
*/

0 commit comments

Comments
 (0)