Skip to content

Commit 62187f7

Browse files
fredrikhrJaKXz
authored andcommitted
feat(types): enforce or infer whether properties are required. (#114)
* Add optional generic constraint for FSA type property * Get jest to also test TypeScript tests * Optional Payload type constraint * Add comments to FSA types and properties * Add FSA extensions with required properties * Simplified FSA TypeScript test Co-Authored-By: couven92 <[email protected]> * fixup! Simplified FSA TypeScript test Fix and refactor TypeScript tests * Add test for FSAAuto type * Update Jest and ESLint dev dependencies * Move generic argument Type to the front for all FSA types
1 parent 2de9019 commit 62187f7

File tree

7 files changed

+1666
-1040
lines changed

7 files changed

+1666
-1040
lines changed

jest.config.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module.exports = {
2+
transform: {
3+
'^.+\\.js$': 'babel-jest',
4+
'^.+\\.tsx?$': 'ts-jest'
5+
},
6+
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.[jt]sx?$',
7+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node']
8+
};

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,21 @@
3333
"@babel/cli": "^7.2.3",
3434
"@babel/core": "^7.3.4",
3535
"@babel/preset-env": "^7.3.4",
36+
"@types/jest": "^24.0.11",
37+
"@typescript-eslint/eslint-plugin": "^1.5.0",
3638
"babel-jest": "^24.5.0",
3739
"babel-plugin-lodash": "^3.3.4",
40+
"eslint": "^5.0.0",
3841
"eslint-config-jest-files": "^0.1.3",
3942
"eslint-config-typescript-basic": "^1.0.1",
4043
"eslint-config-unicorn-camelcase": "^0.1.1",
4144
"eslint-plugin-prettier": "^3.0.1",
42-
"eslint-plugin-typescript": "^0.14.0",
4345
"husky": "^0.14.3",
4446
"jest": "^24.5.0",
4547
"prettier": "^1.16.4",
4648
"pretty-quick": "^1.10.0",
4749
"rimraf": "^2.6.3",
50+
"ts-jest": "^24.0.1",
4851
"typescript": "^3.3.3333",
4952
"typescript-eslint-parser": "^22.0.0",
5053
"xo": "^0.24.0"
@@ -67,7 +70,8 @@
6770
"prettier": {
6871
"singleQuote": true,
6972
"bracketSpacing": true,
70-
"trailingComma": "none"
73+
"trailingComma": "none",
74+
"endOfLine": "auto"
7175
},
7276
"jest": {
7377
"collectCoverage": true,

src/index.d.ts

Lines changed: 207 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1-
export interface FluxStandardAction<Payload, Meta = undefined> {
1+
/**
2+
* A Flux Standard action with optional payload and metadata properties.
3+
*/
4+
export interface FluxStandardAction<
5+
Type extends string = string,
6+
Payload = undefined,
7+
Meta = undefined
8+
> {
29
/**
310
* The `type` of an action identifies to the consumer the nature of the action that has occurred.
411
* Two actions with the same `type` MUST be strictly equivalent (using `===`)
512
*/
6-
type: string;
13+
type: Type;
714
/**
815
* The optional `payload` property MAY be any type of value.
916
* It represents the payload of the action.
@@ -26,36 +33,222 @@ export interface FluxStandardAction<Payload, Meta = undefined> {
2633
meta?: Meta;
2734
}
2835

36+
/**
37+
* An extension of the Flux Standard action that represents an action containing an error as its payload.
38+
*/
2939
export interface ErrorFluxStandardAction<
30-
CustomError extends Error,
40+
Type extends string = string,
41+
CustomError extends Error = Error,
3142
Meta = undefined
32-
> extends FluxStandardAction<CustomError, Meta> {
43+
> extends FluxStandardAction<Type, CustomError, Meta> {
44+
/**
45+
* The required `error` property MUST be set to `true` if the action represents an error.
46+
*/
3347
error: true;
3448
}
3549

3650
/**
3751
* Alias for FluxStandardAction.
3852
*/
39-
export type FSA<Payload, Meta = undefined> = FluxStandardAction<Payload, Meta>;
53+
export type FSA<
54+
Type extends string = string,
55+
Payload = undefined,
56+
Meta = undefined
57+
> = FluxStandardAction<Type, Payload, Meta>;
4058

4159
/**
4260
* Alias for ErrorFluxStandardAction.
4361
*/
4462
export type ErrorFSA<
45-
CustomError extends Error,
46-
Meta = undefined
47-
> = ErrorFluxStandardAction<CustomError, Meta>;
63+
CustomError extends Error = Error,
64+
Meta = undefined,
65+
Type extends string = string
66+
> = ErrorFluxStandardAction<Type, CustomError, Meta>;
4867

4968
/**
5069
* Returns `true` if `action` is FSA compliant.
5170
*/
52-
export function isFSA<Payload, Meta = undefined>(
53-
action: any
54-
): action is FluxStandardAction<Payload, Meta>;
71+
export function isFSA<
72+
Type extends string = string,
73+
Payload = undefined,
74+
Meta = undefined
75+
>(action: any): action is FluxStandardAction<Type, Payload, Meta>;
5576

5677
/**
5778
* Returns `true` if `action` is FSA compliant error.
5879
*/
59-
export function isError<CustomError extends Error, Meta = undefined>(
60-
action: any
61-
): action is ErrorFluxStandardAction<CustomError, Meta>;
80+
export function isError<
81+
Type extends string = string,
82+
CustomError extends Error = Error,
83+
Meta = undefined
84+
>(action: any): action is ErrorFluxStandardAction<Type, CustomError, Meta>;
85+
86+
/**
87+
* A Flux Standard action with a required payload property.
88+
*/
89+
export interface FluxStandardActionWithPayload<
90+
Type extends string = string,
91+
Payload = undefined,
92+
Meta = undefined
93+
> extends FluxStandardAction<Type, Payload, Meta> {
94+
/**
95+
* The required `payload` property MAY be any type of value.
96+
* It represents the payload of the action.
97+
* Any information about the action that is not the type or status of the action should be part of the `payload` field.
98+
* By convention, if `error` is `true`, the `payload` SHOULD be an error object.
99+
* This is akin to rejecting a promise with an error object.
100+
*/
101+
payload: Payload;
102+
}
103+
/**
104+
* Alias for FSAWithPayload
105+
*/
106+
export type FSAWithPayload<
107+
Type extends string = string,
108+
Payload = undefined,
109+
Meta = undefined
110+
> = FluxStandardActionWithPayload<Type, Payload, Meta>;
111+
112+
/**
113+
* A Flux Standard action with a required metadata property.
114+
*/
115+
export interface FluxStandardActionWithMeta<
116+
Type extends string = string,
117+
Payload = undefined,
118+
Meta = undefined
119+
> extends FluxStandardAction<Type, Payload, Meta> {
120+
/**
121+
* The required `meta` property MAY be any type of value.
122+
* It is intended for any extra information that is not part of the payload.
123+
*/
124+
meta: Meta;
125+
}
126+
/**
127+
* Alias for FluxStandardActionWithMeta
128+
*/
129+
export type FSAWithMeta<
130+
Type extends string = string,
131+
Payload = undefined,
132+
Meta = undefined
133+
> = FluxStandardActionWithMeta<Type, Payload, Meta>;
134+
135+
/**
136+
* A Flux Standard action with required payload and metadata properties.
137+
*/
138+
export type FluxStandardActionWithPayloadAndMeta<
139+
Type extends string = string,
140+
Payload = undefined,
141+
Meta = undefined
142+
> = FluxStandardActionWithPayload<Type, Payload, Meta> &
143+
FluxStandardActionWithMeta<Type, Payload, Meta>;
144+
/**
145+
* Alias for FluxStandardActionWithPayloadAndMeta
146+
*/
147+
export type FSAWithPayloadAndMeta<
148+
Type extends string = string,
149+
Payload = undefined,
150+
Meta = undefined
151+
> = FluxStandardActionWithPayloadAndMeta<Type, Payload, Meta>;
152+
153+
/**
154+
* A Flux Standard action with inferred requirements for the payload and metadata properties.
155+
* The `payload` and `meta` properties will be required if the corresponding type argument
156+
* if not the `undefined` type.
157+
*/
158+
export type FluxStandardActionAuto<
159+
Type extends string = string,
160+
Payload = undefined,
161+
Meta = undefined
162+
> = Payload extends undefined
163+
? (Meta extends undefined
164+
? FluxStandardAction<Type, Payload, Meta>
165+
: FluxStandardActionWithMeta<Type, Payload, Meta>)
166+
: (Meta extends undefined
167+
? FluxStandardActionWithPayload<Type, Payload, Meta>
168+
: FluxStandardActionWithPayloadAndMeta<Type, Payload, Meta>);
169+
/**
170+
* Alias for FluxStandardActionAuto
171+
*/
172+
export type FSAAuto<
173+
Type extends string = string,
174+
Payload = undefined,
175+
Meta = undefined
176+
> = FluxStandardActionAuto<Type, Payload, Meta>;
177+
178+
/**
179+
* A Flux Standard Error Action with a required payload property.
180+
*/
181+
export type ErrorFluxStandardActionWithPayload<
182+
Type extends string = string,
183+
CustomError extends Error = Error,
184+
Meta = undefined
185+
> = ErrorFluxStandardAction<Type, CustomError, Meta> &
186+
FluxStandardActionWithPayload<Type, CustomError, Meta>;
187+
/**
188+
* Alias for ErrorFluxStandardActionWithPayload
189+
*/
190+
export type ErrorFSAWithPayload<
191+
Type extends string = string,
192+
CustomError extends Error = Error,
193+
Meta = undefined
194+
> = ErrorFluxStandardActionWithPayload<Type, CustomError, Meta>;
195+
196+
/**
197+
* A Flux Standard Error Action with a required metadata property.
198+
*/
199+
export type ErrorFluxStandardActionWithMeta<
200+
Type extends string = string,
201+
CustomError extends Error = Error,
202+
Meta = undefined
203+
> = ErrorFluxStandardAction<Type, CustomError, Meta> &
204+
FluxStandardActionWithMeta<Type, CustomError, Meta>;
205+
/**
206+
* Alias for ErrorFluxStandardActionWithMeta
207+
*/
208+
export type ErrorFSAWithMeta<
209+
Type extends string = string,
210+
CustomError extends Error = Error,
211+
Meta = undefined
212+
> = ErrorFluxStandardActionWithMeta<Type, CustomError, Meta>;
213+
214+
/**
215+
* A Flux Standard Error Action with required payload and metadata properties.
216+
*/
217+
export type ErrorFluxStandardActionWithPayloadAndMeta<
218+
Type extends string = string,
219+
CustomError extends Error = Error,
220+
Meta = undefined
221+
> = ErrorFluxStandardActionWithPayload<Type, CustomError, Meta> &
222+
ErrorFluxStandardActionWithMeta<Type, CustomError, Meta>;
223+
/**
224+
* Alias for ErrorFluxStandardActionWithPayloadAndMeta
225+
*/
226+
export type ErrorFSAWithPayloadAndMeta<
227+
Type extends string = string,
228+
CustomError extends Error = Error,
229+
Meta = undefined
230+
> = ErrorFluxStandardActionWithPayloadAndMeta<Type, CustomError, Meta>;
231+
232+
/**
233+
* A Flux Standard Error action with inferred requirements for the payload and metadata properties.
234+
* The `payload` and `meta` properties will be required if the corresponding type argument
235+
* if not the `undefined` type.
236+
*
237+
* Note: The `payload` property will always be required, since the `CustomError` type argument does
238+
* not allow for specification of the `undefined` type.
239+
*/
240+
export type ErrorFluxStandardActionAuto<
241+
Type extends string = string,
242+
CustomError extends Error = Error,
243+
Meta = undefined
244+
> = Meta extends undefined
245+
? ErrorFluxStandardActionWithPayload<Type, CustomError, Meta>
246+
: ErrorFluxStandardActionWithPayloadAndMeta<Type, CustomError, Meta>;
247+
/**
248+
* Alias for ErrorFluxStandardActionAuto
249+
*/
250+
export type ErrorFSAAuto<
251+
Type extends string = string,
252+
CustomError extends Error = Error,
253+
Meta = undefined
254+
> = ErrorFluxStandardActionAuto<Type, CustomError, Meta>;

test/fsaAuto.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { FSAAuto } from '../src';
2+
3+
describe('Usage of FSAAuto (automatically infer required properties', () => {
4+
it('must specify payload property even when using a union with undefined', () => {
5+
const fsa_with_payload = { type: 'TEST', payload: undefined };
6+
expectOptionalPayload(fsa_with_payload);
7+
8+
const fsa_without_payload = { type: 'TEST' };
9+
// Not possible to cast!!!
10+
// expectOptionalPayload(fsa_without_payload);
11+
12+
function expectOptionalPayload(fsa: FSAAuto<string, string | undefined>) {
13+
expect(fsa.payload).toBeUndefined();
14+
}
15+
});
16+
});

test/typeFSA.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { FSA } from '../src';
2+
3+
const ACTION_TYPE_1 = 'ACTION_TYPE_1';
4+
type ACTION_TYPE_1 = typeof ACTION_TYPE_1;
5+
type FSA_ACTION_TYPE_1 = FSA<ACTION_TYPE_1>;
6+
7+
const assertNever = (x: never): never => {
8+
throw new Error(`Unexpected value: ${x}.`);
9+
};
10+
11+
const assertTypeValue = (fsa: FSA_ACTION_TYPE_1) => {
12+
expect(fsa.type).toBe(ACTION_TYPE_1);
13+
};
14+
15+
describe('FluxStandardAction<Payload, Meta, Type>', () => {
16+
it('enables TypeScript action type enforcement', () => {
17+
const fsa_strict: FSA_ACTION_TYPE_1 = { type: ACTION_TYPE_1 };
18+
assertTypeValue(fsa_strict);
19+
if (fsa_strict.type !== ACTION_TYPE_1) {
20+
throw assertNever(fsa_strict.type);
21+
}
22+
});
23+
});

tsconfig.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,5 @@
44
"strictNullChecks": true,
55
"target": "es5"
66
},
7-
"files": [
8-
"test/typings.test.ts"
9-
]
7+
"files": ["src/index.d.ts"]
108
}

0 commit comments

Comments
 (0)