Skip to content

Commit

Permalink
feat(types): enforce or infer whether properties are required. (#114)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
fredrikhr authored and JaKXz committed Apr 14, 2019
1 parent 2de9019 commit 62187f7
Show file tree
Hide file tree
Showing 7 changed files with 1,666 additions and 1,040 deletions.
8 changes: 8 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
transform: {
'^.+\\.js$': 'babel-jest',
'^.+\\.tsx?$': 'ts-jest'
},
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.[jt]sx?$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node']
};
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,21 @@
"@babel/cli": "^7.2.3",
"@babel/core": "^7.3.4",
"@babel/preset-env": "^7.3.4",
"@types/jest": "^24.0.11",
"@typescript-eslint/eslint-plugin": "^1.5.0",
"babel-jest": "^24.5.0",
"babel-plugin-lodash": "^3.3.4",
"eslint": "^5.0.0",
"eslint-config-jest-files": "^0.1.3",
"eslint-config-typescript-basic": "^1.0.1",
"eslint-config-unicorn-camelcase": "^0.1.1",
"eslint-plugin-prettier": "^3.0.1",
"eslint-plugin-typescript": "^0.14.0",
"husky": "^0.14.3",
"jest": "^24.5.0",
"prettier": "^1.16.4",
"pretty-quick": "^1.10.0",
"rimraf": "^2.6.3",
"ts-jest": "^24.0.1",
"typescript": "^3.3.3333",
"typescript-eslint-parser": "^22.0.0",
"xo": "^0.24.0"
Expand All @@ -67,7 +70,8 @@
"prettier": {
"singleQuote": true,
"bracketSpacing": true,
"trailingComma": "none"
"trailingComma": "none",
"endOfLine": "auto"
},
"jest": {
"collectCoverage": true,
Expand Down
221 changes: 207 additions & 14 deletions src/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
export interface FluxStandardAction<Payload, Meta = undefined> {
/**
* A Flux Standard action with optional payload and metadata properties.
*/
export interface FluxStandardAction<
Type extends string = string,
Payload = undefined,
Meta = undefined
> {
/**
* The `type` of an action identifies to the consumer the nature of the action that has occurred.
* Two actions with the same `type` MUST be strictly equivalent (using `===`)
*/
type: string;
type: Type;
/**
* The optional `payload` property MAY be any type of value.
* It represents the payload of the action.
Expand All @@ -26,36 +33,222 @@ export interface FluxStandardAction<Payload, Meta = undefined> {
meta?: Meta;
}

/**
* An extension of the Flux Standard action that represents an action containing an error as its payload.
*/
export interface ErrorFluxStandardAction<
CustomError extends Error,
Type extends string = string,
CustomError extends Error = Error,
Meta = undefined
> extends FluxStandardAction<CustomError, Meta> {
> extends FluxStandardAction<Type, CustomError, Meta> {
/**
* The required `error` property MUST be set to `true` if the action represents an error.
*/
error: true;
}

/**
* Alias for FluxStandardAction.
*/
export type FSA<Payload, Meta = undefined> = FluxStandardAction<Payload, Meta>;
export type FSA<
Type extends string = string,
Payload = undefined,
Meta = undefined
> = FluxStandardAction<Type, Payload, Meta>;

/**
* Alias for ErrorFluxStandardAction.
*/
export type ErrorFSA<
CustomError extends Error,
Meta = undefined
> = ErrorFluxStandardAction<CustomError, Meta>;
CustomError extends Error = Error,
Meta = undefined,
Type extends string = string
> = ErrorFluxStandardAction<Type, CustomError, Meta>;

/**
* Returns `true` if `action` is FSA compliant.
*/
export function isFSA<Payload, Meta = undefined>(
action: any
): action is FluxStandardAction<Payload, Meta>;
export function isFSA<
Type extends string = string,
Payload = undefined,
Meta = undefined
>(action: any): action is FluxStandardAction<Type, Payload, Meta>;

/**
* Returns `true` if `action` is FSA compliant error.
*/
export function isError<CustomError extends Error, Meta = undefined>(
action: any
): action is ErrorFluxStandardAction<CustomError, Meta>;
export function isError<
Type extends string = string,
CustomError extends Error = Error,
Meta = undefined
>(action: any): action is ErrorFluxStandardAction<Type, CustomError, Meta>;

/**
* A Flux Standard action with a required payload property.
*/
export interface FluxStandardActionWithPayload<
Type extends string = string,
Payload = undefined,
Meta = undefined
> extends FluxStandardAction<Type, Payload, Meta> {
/**
* The required `payload` property MAY be any type of value.
* It represents the payload of the action.
* Any information about the action that is not the type or status of the action should be part of the `payload` field.
* By convention, if `error` is `true`, the `payload` SHOULD be an error object.
* This is akin to rejecting a promise with an error object.
*/
payload: Payload;
}
/**
* Alias for FSAWithPayload
*/
export type FSAWithPayload<
Type extends string = string,
Payload = undefined,
Meta = undefined
> = FluxStandardActionWithPayload<Type, Payload, Meta>;

/**
* A Flux Standard action with a required metadata property.
*/
export interface FluxStandardActionWithMeta<
Type extends string = string,
Payload = undefined,
Meta = undefined
> extends FluxStandardAction<Type, Payload, Meta> {
/**
* The required `meta` property MAY be any type of value.
* It is intended for any extra information that is not part of the payload.
*/
meta: Meta;
}
/**
* Alias for FluxStandardActionWithMeta
*/
export type FSAWithMeta<
Type extends string = string,
Payload = undefined,
Meta = undefined
> = FluxStandardActionWithMeta<Type, Payload, Meta>;

/**
* A Flux Standard action with required payload and metadata properties.
*/
export type FluxStandardActionWithPayloadAndMeta<
Type extends string = string,
Payload = undefined,
Meta = undefined
> = FluxStandardActionWithPayload<Type, Payload, Meta> &
FluxStandardActionWithMeta<Type, Payload, Meta>;
/**
* Alias for FluxStandardActionWithPayloadAndMeta
*/
export type FSAWithPayloadAndMeta<
Type extends string = string,
Payload = undefined,
Meta = undefined
> = FluxStandardActionWithPayloadAndMeta<Type, Payload, Meta>;

/**
* A Flux Standard action with inferred requirements for the payload and metadata properties.
* The `payload` and `meta` properties will be required if the corresponding type argument
* if not the `undefined` type.
*/
export type FluxStandardActionAuto<
Type extends string = string,
Payload = undefined,
Meta = undefined
> = Payload extends undefined
? (Meta extends undefined
? FluxStandardAction<Type, Payload, Meta>
: FluxStandardActionWithMeta<Type, Payload, Meta>)
: (Meta extends undefined
? FluxStandardActionWithPayload<Type, Payload, Meta>
: FluxStandardActionWithPayloadAndMeta<Type, Payload, Meta>);
/**
* Alias for FluxStandardActionAuto
*/
export type FSAAuto<
Type extends string = string,
Payload = undefined,
Meta = undefined
> = FluxStandardActionAuto<Type, Payload, Meta>;

/**
* A Flux Standard Error Action with a required payload property.
*/
export type ErrorFluxStandardActionWithPayload<
Type extends string = string,
CustomError extends Error = Error,
Meta = undefined
> = ErrorFluxStandardAction<Type, CustomError, Meta> &
FluxStandardActionWithPayload<Type, CustomError, Meta>;
/**
* Alias for ErrorFluxStandardActionWithPayload
*/
export type ErrorFSAWithPayload<
Type extends string = string,
CustomError extends Error = Error,
Meta = undefined
> = ErrorFluxStandardActionWithPayload<Type, CustomError, Meta>;

/**
* A Flux Standard Error Action with a required metadata property.
*/
export type ErrorFluxStandardActionWithMeta<
Type extends string = string,
CustomError extends Error = Error,
Meta = undefined
> = ErrorFluxStandardAction<Type, CustomError, Meta> &
FluxStandardActionWithMeta<Type, CustomError, Meta>;
/**
* Alias for ErrorFluxStandardActionWithMeta
*/
export type ErrorFSAWithMeta<
Type extends string = string,
CustomError extends Error = Error,
Meta = undefined
> = ErrorFluxStandardActionWithMeta<Type, CustomError, Meta>;

/**
* A Flux Standard Error Action with required payload and metadata properties.
*/
export type ErrorFluxStandardActionWithPayloadAndMeta<
Type extends string = string,
CustomError extends Error = Error,
Meta = undefined
> = ErrorFluxStandardActionWithPayload<Type, CustomError, Meta> &
ErrorFluxStandardActionWithMeta<Type, CustomError, Meta>;
/**
* Alias for ErrorFluxStandardActionWithPayloadAndMeta
*/
export type ErrorFSAWithPayloadAndMeta<
Type extends string = string,
CustomError extends Error = Error,
Meta = undefined
> = ErrorFluxStandardActionWithPayloadAndMeta<Type, CustomError, Meta>;

/**
* A Flux Standard Error action with inferred requirements for the payload and metadata properties.
* The `payload` and `meta` properties will be required if the corresponding type argument
* if not the `undefined` type.
*
* Note: The `payload` property will always be required, since the `CustomError` type argument does
* not allow for specification of the `undefined` type.
*/
export type ErrorFluxStandardActionAuto<
Type extends string = string,
CustomError extends Error = Error,
Meta = undefined
> = Meta extends undefined
? ErrorFluxStandardActionWithPayload<Type, CustomError, Meta>
: ErrorFluxStandardActionWithPayloadAndMeta<Type, CustomError, Meta>;
/**
* Alias for ErrorFluxStandardActionAuto
*/
export type ErrorFSAAuto<
Type extends string = string,
CustomError extends Error = Error,
Meta = undefined
> = ErrorFluxStandardActionAuto<Type, CustomError, Meta>;
16 changes: 16 additions & 0 deletions test/fsaAuto.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { FSAAuto } from '../src';

describe('Usage of FSAAuto (automatically infer required properties', () => {
it('must specify payload property even when using a union with undefined', () => {
const fsa_with_payload = { type: 'TEST', payload: undefined };
expectOptionalPayload(fsa_with_payload);

const fsa_without_payload = { type: 'TEST' };
// Not possible to cast!!!
// expectOptionalPayload(fsa_without_payload);

function expectOptionalPayload(fsa: FSAAuto<string, string | undefined>) {
expect(fsa.payload).toBeUndefined();
}
});
});
23 changes: 23 additions & 0 deletions test/typeFSA.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { FSA } from '../src';

const ACTION_TYPE_1 = 'ACTION_TYPE_1';
type ACTION_TYPE_1 = typeof ACTION_TYPE_1;
type FSA_ACTION_TYPE_1 = FSA<ACTION_TYPE_1>;

const assertNever = (x: never): never => {
throw new Error(`Unexpected value: ${x}.`);
};

const assertTypeValue = (fsa: FSA_ACTION_TYPE_1) => {
expect(fsa.type).toBe(ACTION_TYPE_1);
};

describe('FluxStandardAction<Payload, Meta, Type>', () => {
it('enables TypeScript action type enforcement', () => {
const fsa_strict: FSA_ACTION_TYPE_1 = { type: ACTION_TYPE_1 };
assertTypeValue(fsa_strict);
if (fsa_strict.type !== ACTION_TYPE_1) {
throw assertNever(fsa_strict.type);
}
});
});
4 changes: 1 addition & 3 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,5 @@
"strictNullChecks": true,
"target": "es5"
},
"files": [
"test/typings.test.ts"
]
"files": ["src/index.d.ts"]
}
Loading

0 comments on commit 62187f7

Please sign in to comment.