├── .commitlintrc.json
├── .eslintignore
├── .eslintrc.js
├── .github
└── workflows
│ └── node.js.yml
├── .gitignore
├── .husky
├── commit-msg
└── pre-commit
├── .lintstagedrc.js
├── .npmignore
├── .prettierrc.json
├── .versionrc.json
├── CHANGELOG.md
├── README.md
├── index.test-d.ts
├── index.test.ts
├── index.ts
├── jest.config.js
├── package-lock.json
├── package.json
├── tsconfig.json
└── tsconfig.test.json
/.commitlintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["@commitlint/config-conventional"]
3 | }
4 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | .eslintrc.js
4 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es2021: true,
5 | node: true,
6 | jest: true,
7 | },
8 | extends: [
9 | 'eslint:recommended',
10 | 'plugin:@typescript-eslint/eslint-recommended',
11 | 'plugin:@typescript-eslint/recommended',
12 | 'prettier',
13 | 'prettier/@typescript-eslint',
14 | 'plugin:prettier/recommended',
15 | ],
16 | parser: '@typescript-eslint/parser',
17 | parserOptions: {
18 | ecmaVersion: 12,
19 | sourceType: 'module',
20 | },
21 | plugins: ['@typescript-eslint'],
22 | rules: {},
23 | };
24 |
--------------------------------------------------------------------------------
/.github/workflows/node.js.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 | on:
3 | push:
4 | branches: [ master ]
5 | pull_request:
6 | branches: [ master ]
7 |
8 | jobs:
9 | build:
10 |
11 | runs-on: ubuntu-latest
12 |
13 | strategy:
14 | matrix:
15 | node-version: [12.x, 15.x]
16 |
17 | steps:
18 | - uses: actions/checkout@v2
19 | - name: Use Node.js ${{ matrix.node-version }}
20 | uses: actions/setup-node@v1
21 | with:
22 | node-version: ${{ matrix.node-version }}
23 | - run: npm ci
24 | - run: npm run build
25 | - run: npm test
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 | /.idea/
3 | /index.js
4 | /index.d.ts
5 | /index.test.d.ts
6 | /index.test.js
7 | /index.test-d.d.ts
8 | /index.test-d.js
9 | /coverage
10 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx --no-install commitlint --edit $1
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx --no-install lint-staged --relative --concurrent=1
--------------------------------------------------------------------------------
/.lintstagedrc.js:
--------------------------------------------------------------------------------
1 | // The configuration object for [lint-staged](https://www.npmjs.com/package/lint-staged)
2 | module.exports = {
3 | // Lint source with [ESLint](https://www.npmjs.com/package/eslint)
4 | 'index.ts': 'eslint --fix',
5 | // Format all files with [prettier](https://www.npmjs.com/package/prettier).
6 | '*.{ts,js,json,md}': 'prettier --write',
7 | };
8 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /.idea/
2 | /.eslintrc.js
3 | /.gitignore
4 | /.lintstagedrc.js
5 | /.versionrc.json
6 | /jest.config.js
7 | /tsconfig.json
8 | /tsconfig.test.json
9 | /index.ts
10 | /index.test.ts
11 | /index.test.d.ts
12 | /index.test.js
13 | /index.test-d.ts
14 | /index.test-d.d.ts
15 | /index.test-d.js
16 | /coverage
17 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "singleQuote": true,
4 | "trailingComma": "es5"
5 | }
6 |
--------------------------------------------------------------------------------
/.versionrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema#",
3 | "header": "",
4 | "types": [
5 | {
6 | "type": "feat",
7 | "section": "Features"
8 | },
9 | {
10 | "type": "fix",
11 | "section": "Bug Fixes"
12 | },
13 | {
14 | "type": "chore",
15 | "hidden": true
16 | },
17 | {
18 | "type": "docs",
19 | "hidden": true
20 | },
21 | {
22 | "type": "style",
23 | "hidden": true
24 | },
25 | {
26 | "type": "refactor",
27 | "section": "Code Refactoring"
28 | },
29 | {
30 | "type": "perf",
31 | "section": "Performance Improvements"
32 | },
33 | {
34 | "type": "test",
35 | "hidden": true
36 | },
37 | {
38 | "type": "build",
39 | "hidden": true
40 | }
41 | ]
42 | }
43 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | ## 1.0.0 (2020-10-30)
3 |
4 |
5 | ### Features
6 |
7 | * add NonNullish variations for core contracts ([#3](https://github.com/JanMalch/ts-code-contracts/issues/3)) ([93ae61d](https://github.com/JanMalch/ts-code-contracts/commit/93ae61df6f8c941a903fd6af61bd4f28cbb17889))
8 | * add optional message to unreachable ([44d3fb0](https://github.com/JanMalch/ts-code-contracts/commit/44d3fb008d30b638402819fa73d9bc9efb5070c9))
9 | * initial commit ([2b4a3c6](https://github.com/JanMalch/ts-code-contracts/commit/2b4a3c6a960e9598f6ddc9c0a6e448a1c9fe064e))
10 | * remove useIf and improve error ([#5](https://github.com/JanMalch/ts-code-contracts/issues/5)) ([c4ceaf3](https://github.com/JanMalch/ts-code-contracts/commit/c4ceaf358a29a16726a3238a3f9b2713244d663a))
11 | * use IllegalStateError instead of AssertionError ([cc16ac3](https://github.com/JanMalch/ts-code-contracts/commit/cc16ac3549ad888f18a05c0233028530356e664c))
12 |
13 |
14 | ### Bug Fixes
15 |
16 | * adjust meaning of asserts ([32e4c67](https://github.com/JanMalch/ts-code-contracts/commit/32e4c6787d3c63f3d77c9cdf17445bcdbf270d72))
17 | * split error declarations and fix implementation ([e0daabb](https://github.com/JanMalch/ts-code-contracts/commit/e0daabbd73b15282d7cdd89f8cfa360ee3ab8130))
18 | * use AssertionError for unreachable ([ef6a023](https://github.com/JanMalch/ts-code-contracts/commit/ef6a023fe2a4421d63a39daa912de78046dbe7ba))
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ts-code-contracts
2 |
3 | [](https://www.npmjs.com/package/ts-code-contracts)
4 | [](https://github.com/JanMalch/ts-code-contracts/workflows/Build)
5 | [](https://github.com/JanMalch/ts-code-contracts/blob/master/jest.config.js#L14-L17)
6 | [](https://bundlephobia.com/result?p=ts-code-contracts)
7 |
8 | _Design by contract with TypeScript._
9 |
10 | ## Installation & Usage
11 |
12 | ```
13 | npm i ts-code-contracts
14 | ```
15 |
16 | > Requires TypeScript^3.7
17 |
18 | You can now import the following functions `from 'ts-code-contracts'`:
19 |
20 | - [`requires` for preconditions](#requires)
21 | - [`requiresNonNullish` for null-checks as preconditions](#requiresnonnullish)
22 | - [`checks` for invariants](#checks)
23 | - [`checksNonNullish` for null-checks as invariants](#checksnonnullish)
24 | - [`ensures` for postconditions](#ensures)
25 | - [`ensuresNonNullish` for null-checks as postconditions](#ensuresnonnullish)
26 | - [`asserts` for impossible events](#asserts)
27 | - [`unreachable` for unreachable code branches](#unreachable)
28 | - [`error` to make code more concise](#error)
29 | - [`isDefined` type guard](#isdefined)
30 |
31 | Make sure to checkout the examples in the documentation below
32 | or refer to the [test cases](https://github.com/JanMalch/ts-code-contracts/blob/master/index.test.ts#L133-L163)
33 | and [typing assistance](https://github.com/JanMalch/ts-code-contracts/blob/master/index.test-d.ts#L41-L52)!
34 |
35 | Contracts are really just handy shorthands to throw an error, if the given condition is not met.
36 | And yet they greatly help the compiler and the readability of your code.
37 |
38 | ## `requires`
39 |
40 | Use it to validate preconditions, like validating arguments.
41 | Throws a `PreconditionError` if the `condition` is `false`.
42 |
43 | ```ts
44 | function requires(
45 | condition: boolean,
46 | message: string = 'Unmet precondition'
47 | ): asserts condition;
48 | ```
49 |
50 | - `condition` - the condition that should be `true`
51 | - `message` - an optional message for the error
52 |
53 | **Example:**
54 |
55 | ```ts
56 | function myFun(name: string) {
57 | requires(name.length > 10, 'Name must be longer than 10 chars');
58 | }
59 | ```
60 |
61 | ## `requiresNonNullish`
62 |
63 | A variation of `requires` that returns the given value unchanged if it is not `null` or `undefined`.
64 | Throws a `PreconditionError` otherwise.
65 |
66 | ```ts
67 | function requiresNonNullish(
68 | value: T,
69 | message = 'Value must not be null or undefined'
70 | ): NonNullable;
71 | ```
72 |
73 | - `value` - the value that should not be `null` or `undefined`
74 | - `message` - an optional message for the error
75 |
76 | **Example:**
77 |
78 | ```ts
79 | function myFun(name: string | null) {
80 | const nameNonNull = requiresNonNullish(name, 'Name must be defined');
81 | nameNonNull.toUpperCase(); // no compiler error!
82 | }
83 | ```
84 |
85 | ## `checks`
86 |
87 | Use it to check for an illegal state.
88 | Throws a `IllegalStateError` if the `condition` is `false`.
89 |
90 | ```ts
91 | function checks(
92 | condition: boolean,
93 | message = 'Callee invariant violation'
94 | ): asserts condition;
95 | ```
96 |
97 | - `condition` - the condition that should be `true`
98 | - `message` - an optional message for the error
99 |
100 | **Example:**
101 |
102 | ```ts
103 | class Socket {
104 | private isOpen = false;
105 | send(data: Data) {
106 | check(this.isOpen, 'Socket must be open');
107 | }
108 | open() {
109 | this.isOpen = true;
110 | }
111 | }
112 | ```
113 |
114 | ## `checksNonNullish`
115 |
116 | A variation of `checks` that returns the given value unchanged if it is not `null` or `undefined`.
117 | Throws a `IllegalStateError` otherwise.
118 |
119 | ```ts
120 | function checksNonNullish(
121 | value: T,
122 | message = 'Value must not be null or undefined'
123 | ): NonNullable;
124 | ```
125 |
126 | - `value` - the value that should not be `null` or `undefined`
127 | - `message` - an optional message for the error
128 |
129 | **Example:**
130 |
131 | ```ts
132 | class Socket {
133 | data: Data | null = null;
134 | send() {
135 | const validData = checksNonNullish(this.data, 'Data must be available');
136 | validData.send(); // no compiler error!
137 | }
138 | }
139 | ```
140 |
141 | ## `ensures`
142 |
143 | Use it to verify that your code behaved correctly.
144 | Throws a `PostconditionError` if the `condition` is `false`.
145 |
146 | ```ts
147 | function ensures(
148 | condition: boolean,
149 | message = 'Unmet postcondition'
150 | ): asserts condition;
151 | ```
152 |
153 | - `condition` - the condition that should be `true`
154 | - `message` - an optional message for the error
155 |
156 | **Example:**
157 |
158 | ```ts
159 | function myFun() {
160 | createPerson({ id: 0, name: 'John' });
161 | const entity = findById(0); // returns null if not present
162 | return ensures(isDefined(entity), 'Failed to persist entity');
163 | }
164 | ```
165 |
166 | ## `ensuresNonNullish`
167 |
168 | A variation of `ensures` that returns the given value unchanged if it is not `null` or `undefined`.
169 | Throws a `PostconditionError` otherwise.
170 |
171 | ```ts
172 | function ensuresNonNullish(
173 | value: T,
174 | message = 'Value must not be null or undefined'
175 | ): NonNullable;
176 | ```
177 |
178 | - `value` - the value that should not be `null` or `undefined`
179 | - `message` - an optional message for the error
180 |
181 | **Example:**
182 |
183 | ```ts
184 | function myFun(): Person {
185 | createPerson({ id: 0, name: 'John' });
186 | const entity = findById(0); // returns null if not present
187 | return ensuresNonNullish(entity, 'Failed to persist entity');
188 | }
189 | ```
190 |
191 | ## `asserts`
192 |
193 | Clarify that you think that the given condition is impossible to happen.
194 | Throws a `AssertionError` if the `condition` is `false`.
195 |
196 | ```ts
197 | asserts(
198 | condition: boolean,
199 | message?: string
200 | ): asserts condition;
201 | ```
202 |
203 | - `condition` - the condition that should be `true`
204 | - `message` - an optional message for the error
205 |
206 | ## `unreachable`
207 |
208 | Asserts that a code branch is unreachable. If it is, the compiler will throw a type error.
209 | If this function is reached at runtime, an error will be thrown.
210 |
211 | ```ts
212 | function unreachable(
213 | value: never,
214 | message = 'Reached an unreachable case'
215 | ): never;
216 | ```
217 |
218 | - `value` - a value
219 | - `message` - an optional message for the error
220 |
221 | **Example:**
222 |
223 | ```ts
224 | function myFun(foo: MyEnum): string {
225 | switch (foo) {
226 | case MyEnum.A:
227 | return 'a';
228 | case MyEnum.B:
229 | return 'b';
230 | // no compiler error if MyEnum only has A and B
231 | default:
232 | unreachable(foo);
233 | }
234 | }
235 | ```
236 |
237 | ## `error`
238 |
239 | This function will always throw an error.
240 | It helps keeping code easy to read and come in handy when assigning values with a ternary operator or the null-safe operators.
241 |
242 | ```ts
243 | function error(message?: string): never;
244 | function error(
245 | errorType: new (...args: any[]) => Error,
246 | message?: string
247 | ): never;
248 | ```
249 |
250 | - `errorType` - an error class, defaults to `IllegalStateError`
251 | - `message` - an optional message for the error
252 |
253 | **Example:**
254 |
255 | ```ts
256 | function myFun(foo: string | null) {
257 | const bar = foo ?? error(PreconditionError, 'Argument may not be null');
258 | const result = bar.length > 0 ? 'OK' : error('Something went wrong!');
259 | }
260 | ```
261 |
262 | ## `isDefined`
263 |
264 | A type guard, to check that a value is not `null` or `undefined`.
265 | Make sure to use [`strictNullChecks`](https://basarat.gitbook.io/typescript/intro/strictnullchecks).
266 |
267 | ```ts
268 | function isDefined(value: T): value is NonNullable;
269 | ```
270 |
271 | - `value` - the value to test
272 |
273 | **Example:**
274 |
275 | ```ts
276 | const x: string | null = 'Hello';
277 | if (isDefined(x)) {
278 | x.toLowerCase(); // no compiler error!
279 | }
280 | ```
281 |
282 | ## Errors
283 |
284 | The following error classes are included:
285 |
286 | - `PreconditionError` → An error thrown, if a precondition for a function or method is not met.
287 | - `IllegalStateError` → An error thrown, if an object is an illegal state.
288 | - `PostconditionError` → An error thrown, if a function or method could not fulfil a postcondition.
289 | - `AssertionError` → An error thrown, if an assertion has failed.
290 |
--------------------------------------------------------------------------------
/index.test-d.ts:
--------------------------------------------------------------------------------
1 | import { expectType } from 'tsd';
2 | import {
3 | checks,
4 | ensures,
5 | requires,
6 | isDefined,
7 | error,
8 | asserts,
9 | unreachable,
10 | } from './index';
11 |
12 | // CONTRACTS
13 |
14 | function requiresExample(value: string | null) {
15 | requires(isDefined(value));
16 | expectType(value);
17 | }
18 |
19 | function checksExample(value: string | null) {
20 | checks(isDefined(value));
21 | expectType(value);
22 | }
23 |
24 | function ensuresExample(value: string | null) {
25 | ensures(isDefined(value));
26 | expectType(value);
27 | }
28 |
29 | function assertsExample(value: string | null) {
30 | asserts(isDefined(value));
31 | expectType(value);
32 | }
33 |
34 | // UTILS
35 |
36 | function errorExample(value: string | null) {
37 | const result = value ?? error();
38 | expectType(result);
39 | }
40 |
41 | interface Named {
42 | name: string;
43 | }
44 |
45 | function isNamed(value: any): value is Named {
46 | return value != null && typeof value.name === 'string';
47 | }
48 |
49 | function useIfTypeGuardExample(value: any) {
50 | const withName = isNamed(value) ? value : error();
51 | expectType(withName);
52 | }
53 |
54 | function exhaustiveSwitch(foo: 'a' | 'b'): void {
55 | let x;
56 | switch (foo) {
57 | case 'a':
58 | x = 0;
59 | break;
60 | case 'b':
61 | x = 1;
62 | break;
63 | default:
64 | unreachable(foo);
65 | }
66 | expectType(x);
67 | }
68 |
--------------------------------------------------------------------------------
/index.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AssertionError,
3 | asserts,
4 | checks,
5 | checksNonNullish,
6 | ensures,
7 | ensuresNonNullish,
8 | error,
9 | IllegalStateError,
10 | isDefined,
11 | PostconditionError,
12 | PreconditionError,
13 | requires,
14 | requiresNonNullish,
15 | unreachable,
16 | } from './index';
17 |
18 | const expectError = (
19 | fun: () => unknown,
20 | errorType: new (...args: any[]) => Error,
21 | message: string
22 | ) => {
23 | try {
24 | fun();
25 | fail('Function should never succeed');
26 | } catch (e: any) {
27 | expect(e.name).toBe(errorType.name);
28 | expect(e.message).toBe(message);
29 | }
30 | };
31 |
32 | describe('contracts', () => {
33 | const contractTest = (
34 | contract: (condition: boolean, message?: string) => asserts condition,
35 | errorType: new (...args: any[]) => Error,
36 | defaultMessage: string
37 | ) => {
38 | describe(contract.name, () => {
39 | it('should not error if the condition is met', () => {
40 | expect(() => contract(true)).not.toThrowError();
41 | });
42 | it('should throw the associated error if the condition is not met', () => {
43 | expectError(() => contract(false), errorType, defaultMessage);
44 | });
45 | it('should throw the associated error with the given message if the condition is not met', () => {
46 | expectError(
47 | () => contract(false, 'Custom message'),
48 | errorType,
49 | 'Custom message'
50 | );
51 | });
52 | });
53 | };
54 |
55 | contractTest(requires, PreconditionError, 'Unmet precondition');
56 | contractTest(checks, IllegalStateError, 'Callee invariant violation');
57 | contractTest(ensures, PostconditionError, 'Unmet postcondition');
58 | contractTest(asserts, AssertionError, '');
59 | });
60 |
61 | describe('NonNullish contracts', () => {
62 | const contractTest = (
63 | contract: (value: T, message?: string) => NonNullable,
64 | errorType: new (...args: any[]) => Error
65 | ): void => {
66 | describe(contract.name, () => {
67 | it('should not error if the value is defined', () => {
68 | expect(() => contract('A nice String')).not.toThrowError();
69 | });
70 | it('should throw an Error if the value is not defined', () => {
71 | expectError(
72 | () => contract(null),
73 | errorType,
74 | 'Value must not be null or undefined'
75 | );
76 | });
77 | });
78 | };
79 |
80 | contractTest(requiresNonNullish, PreconditionError);
81 | contractTest(checksNonNullish, IllegalStateError);
82 | contractTest(ensuresNonNullish, PostconditionError);
83 | });
84 |
85 | describe('error', () => {
86 | it('should always error', () => {
87 | expectError(() => error(), IllegalStateError, '');
88 | });
89 | it('should error with the given type', () => {
90 | expectError(() => error(PreconditionError), PreconditionError, '');
91 | });
92 | it('should error with the given type and message', () => {
93 | expectError(
94 | () => error(PreconditionError, 'Failed!'),
95 | PreconditionError,
96 | 'Failed!'
97 | );
98 | });
99 | it('should error with the given message', () => {
100 | expectError(() => error('Failed!'), IllegalStateError, 'Failed!');
101 | });
102 | });
103 |
104 | describe('isDefined', () => {
105 | it('should return true for defined values', () => {
106 | expect(isDefined('TypeScript')).toBe(true);
107 | expect(isDefined('')).toBe(true);
108 | expect(isDefined(0)).toBe(true);
109 | expect(isDefined(false)).toBe(true);
110 | });
111 | it('should return false for null-ish values', () => {
112 | expect(isDefined(undefined)).toBe(false);
113 | expect(isDefined(null)).toBe(false);
114 | });
115 | });
116 |
117 | describe('unreachable', () => {
118 | it('should always throw an error at runtime', () => {
119 | expectError(
120 | () => unreachable(true as never),
121 | AssertionError,
122 | 'Reached an unreachable case'
123 | );
124 | });
125 | it('should always throw an error at runtime with the given message', () => {
126 | expectError(
127 | () => unreachable(true as never, 'Test'),
128 | AssertionError,
129 | 'Test'
130 | );
131 | });
132 | it('should not throw an error when the switch is exhaustive', () => {
133 | enum MyEnum {
134 | A,
135 | B,
136 | }
137 |
138 | function myFun(foo: MyEnum): string {
139 | switch (foo) {
140 | case MyEnum.A:
141 | return 'a';
142 | case MyEnum.B:
143 | return 'b';
144 | default:
145 | unreachable(foo);
146 | }
147 | }
148 |
149 | expect(() => myFun(MyEnum.A)).not.toThrow();
150 | expect(() => myFun(MyEnum.B)).not.toThrow();
151 | });
152 | });
153 |
154 | describe('examples', () => {
155 | it('should help with password validation', () => {
156 | // create a "nominal" type and a matching type guard
157 |
158 | /** A nominal type. The `_type` property does not exist at runtime. */
159 | type Nominal = { _type: T } & D;
160 | /** A string that is a valid email address. */
161 | type Email = Nominal<'email', string>;
162 | /** A string that is a good password. */
163 | type Password = Nominal<'password', string>;
164 |
165 | /** Returns `true` if the given value is a valid email address. */
166 | function isEmail(value: string): value is Email {
167 | return !!value && value.includes('@');
168 | }
169 |
170 | /** Returns `true` if the given value meets the requirements. */
171 | function isGoodPassword(value: string): value is Password {
172 | return !!value && value.length >= 8;
173 | }
174 |
175 | // make sure to use the nominal type in later functions
176 | function insert(email: Email, password: Password): void {}
177 |
178 | // the signup endpoint
179 | function signUp(email: string, password: string) {
180 | // use the contracts with your type guards ...
181 | requires(isEmail(email), 'Value must be a valid email address');
182 | requires(isGoodPassword(password), 'Password must meet requirements');
183 | // ... to tell the compiler that email and password are in fact of type Email and Password,
184 | // so that you can call the insert function!
185 | insert(email, password);
186 | }
187 |
188 | expect(() => signUp('type@script.com', 'abc12345')).not.toThrow();
189 | expectError(
190 | () => signUp('typescript.com', 'abc12345'),
191 | PreconditionError,
192 | 'Value must be a valid email address'
193 | );
194 | expectError(
195 | () => signUp('type@script.com', '1234'),
196 | PreconditionError,
197 | 'Password must meet requirements'
198 | );
199 | });
200 | });
201 |
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * An error thrown by a code contract.
3 | */
4 | export abstract class ContractError extends Error {}
5 |
6 | /**
7 | * An error thrown, if a precondition for a function or method is not met.
8 | */
9 | export class PreconditionError extends ContractError {
10 | constructor(message?: string) {
11 | super(message);
12 | this.name = 'PreconditionError';
13 | }
14 | }
15 |
16 | /**
17 | * An error thrown, if an object is an illegal state.
18 | */
19 | export class IllegalStateError extends ContractError {
20 | constructor(message?: string) {
21 | super(message);
22 | this.name = 'IllegalStateError';
23 | }
24 | }
25 |
26 | /**
27 | * An error thrown, if a function or method could not fulfil a postcondition.
28 | */
29 | export class PostconditionError extends ContractError {
30 | constructor(message?: string) {
31 | super(message);
32 | this.name = 'PostconditionError';
33 | }
34 | }
35 |
36 | /**
37 | * An error thrown, if an assertion has failed.
38 | */
39 | export class AssertionError extends ContractError {
40 | constructor(message?: string) {
41 | super(message);
42 | this.name = 'AssertionError';
43 | }
44 | }
45 |
46 | /**
47 | * Throws a `PreconditionError` if the `condition` is `false`.
48 | * @param condition the precondition that should be `true`
49 | * @param message an optional message for the error
50 | * @throws PreconditionError if the condition is `false`
51 | * @see PreconditionError
52 | * @example
53 | * function myFun(name: string) {
54 | * requires(name.length > 10, 'Name must be longer than 10 chars');
55 | * }
56 | */
57 | export function requires(
58 | condition: boolean,
59 | message = 'Unmet precondition'
60 | ): asserts condition {
61 | if (!condition) {
62 | throw new PreconditionError(message);
63 | }
64 | }
65 |
66 | /**
67 | * Returns the given value unchanged if it is not `null` or `undefined`.
68 | * Throws a `PreconditionError` otherwise.
69 | * @param value the value that should not be `null` or `undefined`
70 | * @param message an optional message for the error
71 | * @throws PreconditionError if the value is `null` or `undefined`
72 | * @see requires
73 | * @example
74 | * function myFun(name: string | null) {
75 | * const nameNonNull = requiresNonNullish(name, 'Name must be defined');
76 | * nameNonNull.toUpperCase(); // no compiler error!
77 | * }
78 | */
79 | export function requiresNonNullish(
80 | value: T,
81 | message = 'Value must not be null or undefined'
82 | ): NonNullable {
83 | requires(isDefined(value), message);
84 | return value;
85 | }
86 |
87 | /**
88 | * Throws a `IllegalStateError` if the `condition` is `false`.
89 | * @param condition the condition that should be `true`
90 | * @param message an optional message for the error
91 | * @throws IllegalStateError if the condition is `false`
92 | * @see IllegalStateError
93 | * @example
94 | * class Socket {
95 | * private isOpen = false;
96 | * send(data: Data) {
97 | * check(this.isOpen, 'Socket must be open');
98 | * }
99 | * open() {
100 | * this.isOpen = true;
101 | * }
102 | * }
103 | */
104 | export function checks(
105 | condition: boolean,
106 | message = 'Callee invariant violation'
107 | ): asserts condition {
108 | if (!condition) {
109 | throw new IllegalStateError(message);
110 | }
111 | }
112 |
113 | /**
114 | * Returns the given value unchanged if it is not `null` or `undefined`.
115 | * Throws a `IllegalStateError` otherwise.
116 | * @param value the value that should not be `null` or `undefined`
117 | * @param message an optional message for the error
118 | * @throws IllegalStateError if the value is `null` or `undefined`
119 | * @see checks
120 | * @example
121 | * class Socket {
122 | * data: Data | null = null;
123 | * send() {
124 | * const validData = checksNonNullish(this.data, 'Data must be available');
125 | * validData.send(); // no compiler error!
126 | * }
127 | * }
128 | */
129 | export function checksNonNullish(
130 | value: T,
131 | message = 'Value must not be null or undefined'
132 | ): NonNullable {
133 | checks(isDefined(value), message);
134 | return value;
135 | }
136 |
137 | /**
138 | * Throws a `PostconditionError` if the `condition` is `false`.
139 | * @param condition the condition that should be `true`
140 | * @param message an optional message for the error
141 | * @throws PostconditionError if the condition is `false`
142 | * @see PostconditionError
143 | * @example
144 | * function myFun() {
145 | * createPerson({ id: 0, name: 'John' });
146 | * const entity = findById(0); // returns null if not present
147 | * return ensures(isDefined(entity), 'Failed to persist entity');
148 | * }
149 | */
150 | export function ensures(
151 | condition: boolean,
152 | message = 'Unmet postcondition'
153 | ): asserts condition {
154 | if (!condition) {
155 | throw new PostconditionError(message);
156 | }
157 | }
158 |
159 | /**
160 | * Returns the given value unchanged if it is not `null` or `undefined`.
161 | * Throws a `PostconditionError` otherwise.
162 | * @param value the value that must not be `null` or `undefined`
163 | * @param message an optional message for the error
164 | * @throws PostconditionError if the value is `null` or `undefined`
165 | * @see ensures
166 | * @example
167 | * function myFun(): Person {
168 | * createPerson({ id: 0, name: 'John' });
169 | * const entity = findById(0); // returns null if not present
170 | * return ensuresNonNullish(entity, 'Failed to persist entity');
171 | * }
172 | */
173 | export function ensuresNonNullish(
174 | value: T,
175 | message = 'Value must not be null or undefined'
176 | ): NonNullable {
177 | ensures(isDefined(value), message);
178 | return value;
179 | }
180 |
181 | /**
182 | * Throws a `AssertionError` if the `condition` is `false`.
183 | * @param condition the condition that must be `true`
184 | * @param message an optional message for the error
185 | * @throws AssertionError if the condition is `false`
186 | * @see AssertionError
187 | */
188 | export function asserts(
189 | condition: boolean,
190 | message?: string
191 | ): asserts condition {
192 | if (!condition) {
193 | throw new AssertionError(message);
194 | }
195 | }
196 |
197 | /**
198 | * Returns `true` if the value is not `null` or `undefined`.
199 | * @param value the value to test
200 | * @example
201 | * const x: string | null = 'Hello';
202 | * if (isDefined(x)) {
203 | * x.toLowerCase(); // no compiler error!
204 | * }
205 | */
206 | export function isDefined(value: T): value is NonNullable {
207 | return value != null;
208 | }
209 |
210 | /* eslint-disable @typescript-eslint/no-explicit-any, new-cap */
211 |
212 | /**
213 | * Always throws an `IllegalStateError` with the given message.
214 | * @param message the message for the `IllegalStateError`
215 | * @throws IllegalStateError in any case
216 | * @see IllegalStateError
217 | * @example
218 | * function myFun(foo: string | null) {
219 | * const bar = foo ?? error(PreconditionError, 'Argument may not be null');
220 | * const result = bar.length > 0 ? 'OK' : error('Something went wrong!');
221 | * }
222 | */
223 | export function error(message?: string): never;
224 |
225 | /**
226 | * Always throws an error of the given type with the given message.
227 | * @param errorType an error class
228 | * @param message the error message
229 | * @throws errorType in any case
230 | * @see IllegalStateError
231 | * @example
232 | * function myFun(foo: string | null) {
233 | * const bar = foo ?? error(PreconditionError, 'Argument may not be null');
234 | * const result = bar.length > 0 ? 'OK' : error('Something went wrong!');
235 | * }
236 | */
237 | export function error(
238 | errorType: new (...args: any[]) => Error,
239 | message?: string
240 | ): never;
241 |
242 | export function error(
243 | errorType?: string | (new (...args: any[]) => Error),
244 | message?: string
245 | ): never {
246 | throw errorType == null || typeof errorType === 'string'
247 | ? new IllegalStateError(errorType)
248 | : new errorType(message);
249 | }
250 |
251 | /* eslint-enable @typescript-eslint/no-explicit-any, new-cap */
252 |
253 | /* eslint-disable @typescript-eslint/no-unused-vars */
254 |
255 | /**
256 | * Asserts that a code branch is unreachable. If it is, the compiler will throw a type error.
257 | * If this function is reached at runtime, an error will be thrown.
258 | * @param value a value
259 | * @param message an optional message for the error
260 | * @throws AssertionError in any case
261 | * @example
262 | * function myFun(foo: MyEnum): string {
263 | * switch(foo) {
264 | * case MyEnum.A: return 'a';
265 | * case MyEnum.B: return 'b';
266 | * // no compiler error if MyEnum only has A and B
267 | * default: unreachable(foo);
268 | * }
269 | * }
270 | */
271 | export function unreachable(
272 | value: never,
273 | message = 'Reached an unreachable case'
274 | ): never {
275 | throw new AssertionError(message);
276 | }
277 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('@jest/types').Config.InitialOptions} */
2 | module.exports = {
3 | preset: 'ts-jest',
4 | testEnvironment: 'node',
5 | testRegex: 'index\\.test\\.ts',
6 | globals: {
7 | 'ts-jest': {
8 | tsconfig: 'tsconfig.test.json',
9 | },
10 | },
11 | collectCoverage: true,
12 | coverageThreshold: {
13 | global: {
14 | branches: 100,
15 | functions: 100,
16 | lines: 100,
17 | statements: 100,
18 | },
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ts-code-contracts",
3 | "version": "1.0.0",
4 | "description": "Design by contract with TypeScript.",
5 | "main": "index.js",
6 | "types": "index.d.ts",
7 | "scripts": {
8 | "// TESTS": "tsd needs the index.d.ts file from the build but index.js breaks the test coverage for jest",
9 | "pretest": "npm run build && rimraf index.js",
10 | "test": "jest && tsd",
11 | "build": "tsc -p tsconfig.json",
12 | "lint": "eslint --fix index.ts",
13 | "prettier": "npx prettier --write **/*.{ts,html,scss,json,js,json,md,yaml} --ignore-path .gitignore",
14 | "release": "npx standard-version",
15 | "prepare": "husky install"
16 | },
17 | "files": [
18 | "CHANGELOG.md",
19 | "index.d.ts"
20 | ],
21 | "repository": {
22 | "type": "git",
23 | "url": "git+https://github.com/JanMalch/ts-code-contracts.git"
24 | },
25 | "keywords": [
26 | "Contracts",
27 | "Code Contracts",
28 | "Code Quality",
29 | "TypeScript"
30 | ],
31 | "author": "JanMalch",
32 | "license": "MIT",
33 | "bugs": {
34 | "url": "https://github.com/JanMalch/ts-code-contracts/issues"
35 | },
36 | "homepage": "https://github.com/JanMalch/ts-code-contracts#readme",
37 | "devDependencies": {
38 | "@commitlint/cli": "^18.4.4",
39 | "@commitlint/config-conventional": "^18.4.4",
40 | "@types/jest": "^29.5.11",
41 | "@typescript-eslint/eslint-plugin": "^6.19.0",
42 | "@typescript-eslint/parser": "^6.19.0",
43 | "eslint": "^8.56.0",
44 | "eslint-config-prettier": "^9.1.0",
45 | "eslint-plugin-import": "^2.29.1",
46 | "eslint-plugin-prettier": "^5.1.3",
47 | "husky": "^8.0.3",
48 | "jest": "^29.7.0",
49 | "lint-staged": "^15.2.0",
50 | "prettier": "^3.2.4",
51 | "rimraf": "^5.0.5",
52 | "standard-version": "^9.5.0",
53 | "ts-jest": "^29.1.1",
54 | "tsd": "^0.30.4",
55 | "typescript": "^5.3.3"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 |
5 | /* Basic Options */
6 | // "incremental": true, /* Enable incremental compilation */
7 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
8 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
9 | // "lib": [], /* Specify library files to be included in the compilation. */
10 | // "allowJs": true, /* Allow javascript files to be compiled. */
11 | // "checkJs": true, /* Report errors in .js files. */
12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
13 | "declaration": true /* Generates corresponding '.d.ts' file. */,
14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
15 | // "sourceMap": true, /* Generates corresponding '.map' file. */
16 | // "outFile": "./", /* Concatenate and emit output to single file. */
17 | // "outDir": "./", /* Redirect output structure to the directory. */
18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
19 | // "composite": true, /* Enable project compilation */
20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
21 | // "removeComments": true, /* Do not emit comments to output. */
22 | // "noEmit": true, /* Do not emit outputs. */
23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
26 |
27 | /* Strict Type-Checking Options */
28 | "strict": true /* Enable all strict type-checking options. */,
29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
30 | // "strictNullChecks": true, /* Enable strict null checks. */
31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
36 |
37 | /* Additional Checks */
38 | // "noUnusedLocals": true, /* Report errors on unused locals. */
39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
42 |
43 | /* Module Resolution Options */
44 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
48 | // "typeRoots": [], /* List of folders to include type definitions from. */
49 | // "types": [], /* Type declaration files to be included in compilation. */
50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
51 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
54 |
55 | /* Source Map Options */
56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
60 |
61 | /* Experimental Options */
62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
64 |
65 | /* Advanced Options */
66 | "skipLibCheck": true /* Skip type checking of declaration files. */,
67 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
68 | },
69 | "files": ["index.ts"]
70 | }
71 |
--------------------------------------------------------------------------------
/tsconfig.test.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "inlineSourceMap": true,
5 | "target": "ES6"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------