├── .circleci └── config.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── release.yaml │ └── test.yaml ├── .gitignore ├── .nvmrc ├── .prettierrc.js ├── LICENSE ├── Makefile ├── README.md ├── docs ├── API.md ├── API_TRANSFORMER.md ├── API_TYPE_CHECKER.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── INSTALLATION.md ├── SUPPORTED_TYPES.md └── SUPPORTED_TYPESCRIPT_VERSIONS.md ├── examples ├── .eslintrc.js ├── .gitignore ├── README.md ├── index.ts ├── jest │ ├── example.spec.ts │ ├── jest.config.js │ ├── package.json │ └── tsconfig.json ├── mocha │ ├── .mocharc.json │ ├── example.spec.ts │ ├── mocha.setup.js │ ├── package.json │ └── tsconfig.json ├── package.json ├── rollup │ ├── index.ts │ ├── package.json │ ├── rollup.config.js │ └── tsconfig.json ├── ts-node │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── tsconfig.json ├── ttypescript │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── webpack │ ├── index.ts │ ├── package.json │ ├── tsconfig.json │ └── webpack.config.js └── yarn.lock ├── package.json ├── res ├── ts-type-checked.jpg ├── ts-type-checked.png ├── ts-type-checked.svg └── ts-type-checked@xs.jpg ├── rollup.config.js ├── scripts └── create-version.sh ├── src ├── index.ts ├── package.json ├── runtime.ts └── transformer │ ├── index.ts │ ├── storage │ ├── HashTypeGuardRegistry.ts │ └── MapTypeDescriptorRegistry.ts │ ├── typeDescriptor │ ├── typeDescriptorGenerator.ts │ └── utils │ │ ├── assert.ts │ │ ├── getDOMElementClassName.ts │ │ ├── getFirstValidDeclaration.ts │ │ ├── getLibraryTypeDescriptorName.ts │ │ ├── getPropertyTypeDescriptors.ts │ │ └── messages.ts │ ├── typeGuard │ ├── typeGuardAsserter.ts │ ├── typeGuardGenerator.ts │ ├── typeGuardResolver.ts │ └── utils │ │ └── codeGenerators.ts │ ├── typeName │ ├── debugTypeNameGenerator.ts │ ├── productionTypeNameGenerator.ts │ └── typeNameResolver.ts │ ├── types.ts │ ├── utils │ ├── ast.ts │ ├── codeGenerators.ts │ ├── debug.ts │ ├── logger.ts │ ├── transformUsingVisitor.ts │ └── transformerOptions.ts │ └── visitor │ ├── assertions.ts │ └── typeCheckVisitor.ts ├── test ├── .eslintrc.js ├── jest.config.js ├── package.json ├── scripts │ ├── list-setups-for-typescript.js │ ├── test-with-every-version.sh │ ├── test-with-version.sh │ └── versions.txt ├── setups │ ├── issue-43--strict-mode │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── tests │ │ │ └── withStrict.spec.ts │ │ └── tsconfig.json │ ├── react │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── tests │ │ │ └── react.spec.tsx │ │ └── tsconfig.json │ ├── typescript--2.7.2 │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── tests │ │ │ ├── arrays.spec.ts │ │ │ ├── basics.spec.ts │ │ │ ├── classes.spec.ts │ │ │ ├── dom.spec.ts │ │ │ ├── enums.spec.ts │ │ │ ├── indexed.spec.ts │ │ │ ├── interfaces.spec.ts │ │ │ ├── literals.spec.ts │ │ │ ├── map.spec.ts │ │ │ ├── promise.spec.ts │ │ │ ├── resolution.spec.ts │ │ │ ├── set.spec.ts │ │ │ └── special-cases.spec.ts │ │ └── tsconfig.json │ ├── typescript--2.8.3 │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── tests │ │ │ └── conditional.spec.ts │ │ └── tsconfig.json │ ├── typescript--2.9.1 │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── tests │ │ │ └── indexed.spec.ts │ │ └── tsconfig.json │ ├── typescript--3.0.1 │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── tests │ │ │ └── unknown.spec.ts │ │ └── tsconfig.json │ ├── typescript--3.2.1 │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── tests │ │ │ └── bigint.spec.ts │ │ └── tsconfig.json │ ├── typescript--3.8.2 │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── tests │ │ │ └── classes.spec.ts │ │ └── tsconfig.json │ ├── typescript--4.0.2 │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── tests │ │ │ └── literals.spec.ts │ │ └── tsconfig.json │ └── without-strict-mode │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── tests │ │ └── withoutStrictMode.spec.ts │ │ └── tsconfig.json ├── tsconfig.json ├── utils │ ├── utils.v2.ts │ └── utils.v3.ts └── yarn.lock ├── tsconfig.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | node: circleci/node@3.0.1 5 | 6 | # Job defaults 7 | defaults: &defaults 8 | working_directory: ~/project 9 | executor: 10 | name: node/default 11 | 12 | # Filters for jobs that only need to be run for a version tag (release) 13 | only-release: &only-release 14 | filters: 15 | # Ignore all branches 16 | branches: 17 | ignore: /.*/ 18 | # And only run on version tags 19 | tags: 20 | only: /^v(\d+)\.(\d+)\.(\d+).*/ 21 | 22 | jobs: 23 | build: 24 | <<: *defaults 25 | steps: 26 | # Checkout the project 27 | - checkout 28 | 29 | # Restore NPM modules from cache 30 | - restore_cache: 31 | keys: 32 | - v1-root-{{ checksum "yarn.lock" }} 33 | - v1-root 34 | 35 | - run: 36 | name: Install dependencies 37 | command: make install 38 | 39 | - run: 40 | name: Build 41 | command: make build 42 | 43 | - run: 44 | name: Lint 45 | command: make lint 46 | 47 | - persist_to_workspace: 48 | root: ~/project 49 | paths: 50 | - dist 51 | 52 | # Restore NPM modules from cache 53 | - save_cache: 54 | key: v1-root-{{ checksum "yarn.lock" }} 55 | paths: 56 | - node_modules 57 | 58 | e2e-test: 59 | <<: *defaults 60 | steps: 61 | # Checkout the project 62 | - checkout 63 | 64 | # Get the build artifacts 65 | - attach_workspace: 66 | at: ~/project 67 | 68 | - restore_cache: 69 | keys: 70 | - v1-test-{{ checksum "test/yarn.lock" }} 71 | - v1-test 72 | 73 | - run: 74 | name: Test package 75 | command: make test 76 | 77 | - save_cache: 78 | key: v1-test-{{ checksum "test/yarn.lock" }} 79 | paths: 80 | - test/node_modules 81 | 82 | - restore_cache: 83 | keys: 84 | - v1-examples-{{ checksum "examples/yarn.lock" }} 85 | - v1-examples 86 | 87 | - run: 88 | name: Test examples 89 | command: make examples 90 | 91 | - save_cache: 92 | key: v1-examples-{{ checksum "examples/yarn.lock" }} 93 | paths: 94 | - examples/node_modules 95 | 96 | publish: 97 | <<: *defaults 98 | steps: 99 | # Checkout the project 100 | - checkout 101 | 102 | # Get the build artifacts 103 | - attach_workspace: 104 | at: ~/project 105 | 106 | - run: 107 | name: Authenticate for NPM 108 | command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > dist/.npmrc 109 | 110 | - run: 111 | name: Publish package 112 | command: make publish 113 | 114 | workflows: 115 | test: 116 | jobs: 117 | - build 118 | - e2e-test: 119 | requires: 120 | - build 121 | 122 | release: 123 | jobs: 124 | - build: 125 | <<: *only-release 126 | 127 | - e2e-test: 128 | <<: *only-release 129 | requires: 130 | - build 131 | 132 | - approve: 133 | <<: *only-release 134 | type: approval 135 | requires: 136 | - e2e-test 137 | 138 | - publish: 139 | <<: *only-release 140 | requires: 141 | - approve 142 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | examples/index.js 3 | examples/**/index.js -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser 3 | extends: [ 4 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from @typescript-eslint/eslint-plugin 5 | 'plugin:prettier/recommended', 6 | ], 7 | plugins: ['sort-imports-es6-autofix'], 8 | parserOptions: { 9 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features 10 | sourceType: 'module', // Allows for the use of imports 11 | ecmaFeatures: { 12 | jsx: true, 13 | }, 14 | }, 15 | overrides: [ 16 | { 17 | files: ['*.js', '*.jsx'], 18 | rules: { 19 | // And as mentioned here this rule will freak out on .js files as well 20 | // https://github.com/typescript-eslint/typescript-eslint/issues/906 21 | // 22 | // So we disable it for .js files using overrides 23 | '@typescript-eslint/explicit-function-return-type': 0, 24 | 25 | // And the same goes for member accessibility 26 | // 27 | // See https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/explicit-member-accessibility.md 28 | '@typescript-eslint/explicit-member-accessibility': 0, 29 | 30 | // And last but not least require() calls are enabled in js files 31 | '@typescript-eslint/no-var-requires': 0, 32 | }, 33 | }, 34 | ], 35 | rules: { 36 | // Prevent forgotten console.* statements 37 | 'no-console': 1, 38 | 39 | // Make sure imports get sorted 40 | 'sort-imports-es6-autofix/sort-imports-es6': [ 41 | 2, 42 | { 43 | ignoreCase: false, 44 | ignoreMemberSort: false, 45 | memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'], 46 | }, 47 | ], 48 | 49 | '@typescript-eslint/no-use-before-define': [1, { functions: false }], 50 | 51 | '@typescript-eslint/explicit-function-return-type': 0, 52 | }, 53 | settings: { 54 | 'import/resolver': { 55 | node: { 56 | extensions: ['.ts', '.js'], 57 | }, 58 | }, 59 | }, 60 | }; 61 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | # This workflow is triggered manually 4 | on: workflow_dispatch 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | 12 | # Module cache setup 13 | - name: Cache node modules 14 | uses: actions/cache@v2 15 | with: 16 | path: ~/.npm 17 | key: v1-npm-deps-${{ hashFiles('**/yarn.lock') }} 18 | restore-keys: v1-npm-deps- 19 | 20 | # Module installation 21 | - name: Install dependencies 22 | run: make install 23 | 24 | # The actual build 25 | - name: Build project 26 | run: make build 27 | 28 | # Now let's store the build 29 | - name: Store build artefacts 30 | uses: actions/upload-artifact@v2 31 | with: 32 | name: build 33 | path: dist 34 | 35 | publish: 36 | runs-on: ubuntu-latest 37 | needs: build 38 | environment: 39 | name: Public Github 40 | url: https://www.npmjs.com/package/ts-type-checked?activeTab=versions 41 | steps: 42 | # Get the build 43 | - name: Download the build 44 | uses: actions/download-artifact@v2 45 | with: 46 | name: build 47 | path: dist 48 | 49 | # Publish to NPM [DRY RUN] 50 | - uses: JS-DevTools/npm-publish@v1 51 | with: 52 | package: ./dist/package.json 53 | token: ${{ secrets.NPM_TOKEN }} 54 | check-version: true 55 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | # Controls when the action will run. 4 | on: 5 | [push] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | # Module cache setup 14 | - name: Cache node modules 15 | uses: actions/cache@v2 16 | with: 17 | path: ~/.npm 18 | key: v1-npm-deps-${{ hashFiles('**/yarn.lock') }} 19 | restore-keys: v1-npm-deps- 20 | 21 | # Module installation 22 | - name: Install dependencies 23 | run: make install 24 | 25 | # The actual build 26 | - name: Build project 27 | run: make build 28 | 29 | # Some linting 30 | - name: Lint project 31 | run: make lint 32 | 33 | # Now let's store the build 34 | - name: Store build artefacts 35 | uses: actions/upload-artifact@v2 36 | with: 37 | name: build 38 | path: dist 39 | 40 | test: 41 | runs-on: ubuntu-latest 42 | needs: build 43 | steps: 44 | # Checks-out your repository under $GITHUB_WORKSPACE 45 | - uses: actions/checkout@v2 46 | 47 | # Module cache setup 48 | - name: Cache node modules 49 | uses: actions/cache@v2 50 | with: 51 | path: ~/.npm 52 | key: v1-npm-deps-${{ hashFiles('**/yarn.lock') }} 53 | restore-keys: v1-npm-deps- 54 | 55 | # Get the build 56 | - name: Download the build 57 | uses: actions/download-artifact@v2 58 | with: 59 | name: build 60 | path: dist 61 | 62 | # Test 63 | - name: Test 64 | run: make test 65 | 66 | # Test examples 67 | - name: Examples 68 | run: make examples 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log* 2 | node_modules 3 | dist 4 | 5 | # This is where the test project is executed 6 | sandbox 7 | 8 | examples/index.js 9 | examples/index.*.js 10 | 11 | *.log 12 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 12.13.0 -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | printWidth: 120, 6 | tabWidth: 2, 7 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ján Jakub Naništa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all test examples 2 | 3 | # The default target that builds the package 4 | all: build lint 5 | 6 | # Installs NPM dependencies for the root project and for the test and examples "workspaces" 7 | install: 8 | yarn 9 | 10 | # Clear all installed NPM dependencies 11 | uninstall: 12 | find . -name "node_modules" -type d -exec rm -rf '{}' + 13 | 14 | # Upgrade NPM dependencies 15 | upgrade: 16 | yarn upgrade --latest 17 | cd test; yarn upgrade --latest 18 | cd examples; yarn upgrade --latest 19 | 20 | # Build project 21 | build: 22 | yarn rollup -c 23 | 24 | # Build and watch project 25 | watch: 26 | yarn rollup -cw 27 | 28 | # Clean built project 29 | clean: 30 | rm -rf dist 31 | 32 | # Lint (source & built) 33 | lint: 34 | yarn eslint . --fix --ext .js,.ts 35 | 36 | # Run the complete test suite from test project 37 | test: 38 | cd test; yarn 39 | cd test; yarn test 40 | 41 | # Check whether the examples still work 42 | examples: 43 | cd examples; yarn 44 | cd examples; yarn test 45 | 46 | # Publish built package 47 | publish: 48 | cd dist; npm publish 49 | 50 | release: clean build lint test examples publish 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 |

5 | 6 |

7 | ts-type-checked 8 |

9 | 10 |

11 | Automatic type guards for TypeScript 12 |

13 | 14 | 15 |

16 | 17 | GitHub build status 18 | 19 | NPM Version 20 | 21 | Dev Dependency Status 22 | 23 | Known Vulnerabilities 24 | 25 | License 26 |

27 | 28 |

29 | ts-type-checked generates type guards based on your own (or library) TypeScript types. 30 | It is compatible with 31 | Rollup, 32 | Webpack and 33 | ttypescript projects 34 | and works nicely with 35 | Jest, 36 | Mocha or 37 | ts-node 38 |

39 | 40 |

41 | Example cases 42 | | 43 | Installation 44 | | 45 | API 46 | | 47 | Supported types 48 |

49 | 50 | ## Wait what? 51 | 52 | As they say *an example is worth a thousand API docs* so why not start with one. 53 | 54 | ```typescript 55 | interface WelcomeMessage { 56 | name: string; 57 | hobbies: string[]; 58 | } 59 | 60 | // 61 | // You can now turn this 62 | // 63 | const isWelcomeMessage = (value: any): message is WelcomeMessage => 64 | !!value && 65 | typeof value.name === 'string' && 66 | Array.isArray(value.hobbies) && 67 | value.hobbies.every(hobby => typeof hobby === 'string'); 68 | 69 | // 70 | // Into this 71 | // 72 | const isWelcomeMessage = typeCheckFor(); 73 | 74 | // 75 | // Or without creating a function 76 | // 77 | if (isA(value)) { 78 | // value is a WelcomeMessage! 79 | } 80 | ``` 81 | 82 | ## Motivation 83 | 84 | TypeScript is a powerful way of enhancing your application code at compile time but, unfortunately, provides no runtime type guards out of the box - you need to [create these manually](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards). For types like `string` or `boolean` this is easy, you can just use the `typeof` operator. It becomes more difficult for interface types, arrays, enums etc. 85 | 86 | **And that is where `ts-type-checked` comes in!** It automatically creates these type guards at compile time for you. 87 | 88 | This might get useful when: 89 | 90 | - You want to make sure an object you received from an API matches the expected type 91 | - You are exposing your code as a library and want to prevent users from passing in invalid arguments 92 | - You want to check whether a variable implements one of more possible interfaces (e.g. `LoggedInUser`, `GuestUser`) 93 | - _..._ 94 | 95 | ## Example cases 96 | 97 | ### Checking external data 98 | 99 | Imagine your [API spec](https://swagger.io/) promises to respond with objects like these: 100 | 101 | ```typescript 102 | interface WelcomeMessage { 103 | name: string; 104 | greeting: string; 105 | } 106 | 107 | interface GoodbyeMessage { 108 | sayByeTo: string[]; 109 | } 110 | ``` 111 | 112 | Somewhere in your code there probably is a function just like `handleResponse` below: 113 | 114 | ```typescript 115 | function handleResponse(data: string): string { 116 | const message = JSON.parse(data); 117 | 118 | if (isWelcomeMessage(message)) { 119 | return 'Good day dear ' + message.name! 120 | } 121 | 122 | if (isGoodbyeMessage(message)) { 123 | return 'I will say bye to ' + message.sayByeTo.join(', '); 124 | } 125 | 126 | throw new Error('I have no idea what you mean'); 127 | } 128 | ``` 129 | 130 | If you now need to find out whether you received a valid response, you end up defining helper functions like `isWelcomeMessage` and `isGoodbyeMessage` below. 131 | 132 | ```typescript 133 | const isWelcomeMessage = (value: any): value is WelcomeMessage => 134 | !!value && 135 | typeof value.name === 'string' && 136 | typeof value.greeting === 'string'; 137 | 138 | const isGoodbyeMessage = (value: any): value is GoodbyeMessage => 139 | !!value && 140 | Array.isArray(value.sayByeTo) && 141 | value.sayByeTo.every(name => typeof name === 'string'); 142 | ``` 143 | 144 | Annoying isn't it? Not only you need to define the guards yourself, you also need to make sure the types and the type guards don't drift apart as the code evolves. Let's try using `ts-type-checked`: 145 | 146 | ```typescript 147 | import { isA, typeCheckFor } from 'ts-type-checked'; 148 | 149 | // You can use typeCheckFor type guard factory 150 | const isWelcomeMessage = typeCheckFor(); 151 | const isGoodbyeMessage = typeCheckFor(); 152 | 153 | // Or use isA generic type guard directly in your code 154 | if (isA(message)) { 155 | // ... 156 | } 157 | ``` 158 | 159 | ### Type guard factories 160 | 161 | `ts-type-checked` exports `typeCheckFor` type guard factory. This is more or less a syntactic sugar that saves you couple of keystrokes. It is useful when you want to store the type guard into a variable or pass it as a parameter: 162 | 163 | ```typescript 164 | import { typeCheckFor } from 'ts-type-checked'; 165 | 166 | interface Config { 167 | version: string; 168 | } 169 | 170 | const isConfig = typeCheckFor(); 171 | const isString = typeCheckFor(); 172 | 173 | function handleArray(array: unknown[]) { 174 | const strings = array.filter(isString); 175 | const configs = array.filter(isConfig); 176 | } 177 | 178 | // Without typeCheckFor you'd need to write 179 | const isConfig = (value: unknown): value is Config => isA(value); 180 | const isString = (value: unknown): value is Config => isA(value); 181 | ``` 182 | 183 | ### Reducing the size of generated code 184 | 185 | `isA` and `typeCheckFor` will both transform the code on per-file basis - in other terms a type guard function will be created in every file where either of these is used. To prevent duplication of generated code I recommend placing the type guards in a separate file and importing them when necessary: 186 | 187 | ```typescript 188 | // in file typeGuards.ts 189 | import { typeCheckFor } from 'ts-type-checked'; 190 | 191 | export const isDate = typeCheckFor(); 192 | export const isStringRecord = typeCheckFor>(); 193 | 194 | // in file myUtility.ts 195 | import { isDate } from './typeGuards'; 196 | 197 | if (isDate(value)) { 198 | // ... 199 | } 200 | ``` 201 | 202 | ## Useful links 203 | 204 | - [ts-trasformer-keys](https://www.npmjs.com/package/ts-transformer-keys), TypeScript transformer that gives you access to interface properties 205 | - [ts-auto-mock](https://www.npmjs.com/package/ts-auto-mock), TypeScript transformer that generates mock data objects based on your types 206 | -------------------------------------------------------------------------------- /docs/API.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | ts-type-checked 4 |

5 | 6 | < Back to project 7 | 8 | # API 9 | 10 | `ts-type-checked` has two APIs: 11 | 12 | - [Type guard API](./API_TYPE_CHECKER.md) that you use in your code - the `isA` and `typeCheckFor` functions 13 | - [Transformer API](./API_TRANSFORMER.md) for integration with [Webpack](./INSTALLATION.md#installation--webpack), [Rollup](./INSTALLATION.md#installation--rollup), [ttypescript](./INSTALLATION.md#installation--ttypescript), [Jest](./INSTALLATION.md#installation--jest), [Mocha](./INSTALLATION.md#installation--jest) and [ts-node](./INSTALLATION.md#installation--ts-node) -------------------------------------------------------------------------------- /docs/API_TRANSFORMER.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | ts-type-checked 4 |

5 | 6 | < Back to project 7 | 8 | # Transformer API 9 | 10 | ## `function transformer(program: ts.Program, options: TransformerOptions): (file: ts.SourceFile) => ts.SourceFile` 11 | 12 | The transformer function is exported from `ts-type-checked/transformer`: 13 | 14 | ```typescript 15 | import transformer from 'ts-type-checked/transformer'; 16 | 17 | // Or equivalent 18 | const transfomer = require('ts-type-checked/transformer').default; 19 | ``` 20 | 21 | Please refer to the [installation section](./INSTALLATION.md) for more information on how to plug the transformer into your build. 22 | 23 | ### TransformerOptions 24 | 25 | `transformer` function accepts an `options` object with the following keys: 26 | 27 | |Name|Type|Default value|Description| 28 | |----|----|-------------|-----------| 29 | |`logLevel`|`'debug'`
`'normal'`
`'nosey'`
`'silent'`
`'normal'`|Set the verbosity of logging when transforming| 30 | |`mode`|`'development'`
`'production'`|`process.env.NODE_ENV`|In `production` mode the generated code is slightly smaller (the code generator does not use full names of types in the output)| 31 | 32 | ## Passing options to `transformer` 33 | 34 | Depending on the type of your project there are several ways of passing the `options` to the transformer. 35 | 36 | ### Webpack and Rollup projects 37 | 38 | You can pass options to the transformer directly in your config file: 39 | 40 | ```javascript 41 | // In your Webpack config loader configuration 42 | { 43 | // ... 44 | getCustomTransformers: program => ({ 45 | before: [transformer(program, { logLevel: 'debug' })], 46 | }), 47 | // ... 48 | } 49 | 50 | // In your Rollup config using @wessberg/rollup-plugin-ts 51 | ts({ 52 | transformers: [ 53 | ({ program }) => ({ 54 | before: transformer(program, { logLevel: 'debug' }), 55 | }), 56 | ], 57 | }), 58 | 59 | // In your Rollup config using rollup-plugin-typescript2 60 | typescript({ 61 | transformers: [ 62 | service => ({ 63 | before: [transformer(service.getProgram(), { logLevel: 'debug' })], 64 | after: [], 65 | }), 66 | ], 67 | }), 68 | ``` 69 | 70 | ### TTypeScript projects 71 | 72 | You can pass options to the transformer via `tsconfig.json`: 73 | 74 | ```javascript 75 | { 76 | // ... 77 | "compilerOptions": { 78 | "plugins": [ 79 | { "transform": "ts-type-checked/transformer", "logLevel": "debug" }, 80 | ] 81 | }, 82 | // ... 83 | } 84 | ``` -------------------------------------------------------------------------------- /docs/API_TYPE_CHECKER.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | ts-type-checked 4 |

5 | 6 | < Back to project 7 | 8 | # Type checker API 9 | 10 | Two functions are exported: `isA` and `typeCheckFor`. (funny enough neither of them exist, just check [index.js](https://github.com/janjakubnanista/ts-type-checked/tree/main/src/index.ts) yourself :grinning:). 11 | 12 | ```typescript 13 | import { isA, typeCheckFor } from 'ts-type-checked'; 14 | ``` 15 | 16 | #### `function isA(value: unknown): value is T` 17 | 18 | `isA` takes one type argument `T` and one function argument `value` and checks whether the value is assignable to type `T`: 19 | 20 | ```typescript 21 | if (isA(valueFromApi)) { 22 | // valueFromApi is now for sure a string array! 23 | } 24 | 25 | interface MyInterface { 26 | name?: string; 27 | items: string[]; 28 | } 29 | 30 | if (isA(value)) { 31 | // value is MyInterface 32 | } 33 | ``` 34 | 35 | **The type that you pass to `isA` must not be a type parameter!** In other words: 36 | 37 | ```typescript 38 | function doMyStuff(value: unknown) { 39 | // Bad, T is a type argument and will depend on how you call the function 40 | if (isA(value)) { 41 | // ... 42 | } 43 | 44 | // Good, string[] is not a type parameter 45 | if (isA(value)) { 46 | // ... 47 | } 48 | } 49 | ``` 50 | 51 | #### `function typeCheckFor(): (value: unknown) => value is T` 52 | 53 | `typeCheckFor` is a factory function for `isA` so to say - it takes one type argument `T` and returns a function, just like `isA`, that takes an argument `value` and checks whether the value is assignable to type `T`. 54 | 55 | **The type that you pass to `typeCheckFor` must not be a type parameter either!** (see above) 56 | 57 | -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | ts-type-checked 4 |

5 | 6 | < Back to project 7 | 8 | # Changelog 9 | 10 | ## v0.4.0 11 | 12 | ### Features 13 | 14 | - Support for `symbol` and `Symbol` types 15 | 16 | ### Bugfixes 17 | 18 | - `private` and `protected` members are not checked anymore 19 | 20 | ## v0.4.1 21 | 22 | ### Bugfixes 23 | 24 | - Checking circular structures will not end up with stack overflow anymore, instead an `Error` will be thrown when a circular structure is being checked 25 | 26 | ## v0.4.2 27 | 28 | ### Bugfixes 29 | 30 | - Transformer would throw an error in older versions of TypeScript due to missing `isArrayType` method on `ts.TypeChecker` 31 | 32 | ## v0.5.0 33 | 34 | ### Features 35 | 36 | - Add support for codebases without `strictNullChecks` enabled in `tsconfig.json`. **Without `strictNullChecks`, `ts-type-checkd` is not very useful since it will produce a lot of false positives!** Please consider turning `strictNullChecks` on. 37 | 38 | ## v0.6.2 39 | 40 | ### Bugs 41 | 42 | - #46 Object keys that contain dashes produce errors 43 | 44 | ## v0.6.5 45 | 46 | ### Bugs 47 | 48 | - #79 Boolean literals were not recognised in TypeScript 4 49 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | ts-type-checked 4 |

5 | 6 | < Back to project 7 | 8 | # Contributing 9 | 10 | ## Development 11 | 12 | Project comes with a `Makefile` that defines the following targets: 13 | 14 | - `all` (default) equivalent to `make build` and `make lint` 15 | - `build` Builds the package to `./dist` folder. This will result in a publishable artifact 16 | - `watch` Builds the package to `./dist` folder and watches for file changes, rebuilding every time 17 | - `install` Installs NPM dependencies for the package. Will not install dependencies for `test` or `examples` projects since they depend on the build artifact of the root project 18 | - `uninstall` Remove all `node_modules` folders recursively 19 | - `upgrade` Upgrade dependencies of all projects to their latest versions 20 | - `clean` Remove the `./dist` folder 21 | - `lint` ESLint the whole project 22 | - `test` Run the complete test suite in `test` project. This will run the tests against all the TypeScript versions specified in [versions.txt](https://github.com/janjakubnanista/ts-type-checked/blob/main/test/scripts/versions.txt). To run a specific test suite, specific test or against a specific TypeScript version, you need to check the `test` project yarn command `test:version` and `test-with-version.sh` script. 23 | - `examples` Builds and runs the `examples` project -------------------------------------------------------------------------------- /docs/INSTALLATION.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | ts-type-checked 4 |

5 | 6 | < Back to project 7 | 8 | # Installation 9 | 10 |

11 | Webpack 12 | | 13 | Rollup 14 | | 15 | ttypescript 16 | | 17 | Jest 18 | | 19 | Mocha 20 | | 21 | ts-node 22 |

23 | 24 | `ts-type-checked` is a TypeScript transformer - it generates the required type checks and injects them into your code at compile time. It is compatible with [Webpack](#installation--webpack), [Rollup](#installation--rollup), and [ttypescript](#installation--ttypescript) projects and works nicely with [Jest](#installation--jest), [Mocha](#installation--mocha) and [ts-node](#installation--ts-node). 25 | 26 | You will first need to install `ts-type-checked` using `npm`, `yarn` or similar: 27 | 28 | ```bash 29 | # NPM 30 | npm install --dev ts-type-checked 31 | 32 | # Yarn 33 | yarn add -D ts-type-checked 34 | ``` 35 | 36 | 37 | ## Webpack 38 | 39 | [See example here](https://github.com/janjakubnanista/ts-type-checked/tree/main/examples/webpack) 40 | 41 | In order to enable `ts-type-checked` in your Webpack project you need to configure `ts-loader` or `awesome-typescript-loader` in you Webpack config. 42 | 43 | ### 1. Import the transformer 44 | 45 | ```typescript 46 | // Using ES6 imports 47 | import transformer from 'ts-type-checked/transformer'; 48 | 49 | // Or using the old syntax 50 | const transformer = require('ts-type-checked/transformer').default; 51 | ``` 52 | 53 | ### 2. Adjust your `ts-loader` / `awesome-typescript-loader` configuration 54 | 55 | ```typescript 56 | { 57 | test: /\.ts(x)?$/, 58 | loader: 'ts-loader', // Or 'awesome-typescript-loader' 59 | options: { 60 | getCustomTransformers: program => ({ 61 | before: [transformer(program)], 62 | }), 63 | }, 64 | } 65 | ``` 66 | 67 | 68 | ## Rollup 69 | 70 | [See example here](https://github.com/janjakubnanista/ts-type-checked/tree/main/examples/rollup) 71 | 72 | In order to enable `ts-type-checked` in your Rollup project you need to configure `ts-loader` or `awesome-typescript-loader` in you rollup config. 73 | 74 | ### 1. Import the transformer 75 | 76 | ```typescript 77 | import transformer from 'ts-type-checked/transformer'; 78 | ``` 79 | 80 | ### 2. Option 1: Adjust your `@wessberg/rollup-plugin-ts` plugin configuration 81 | 82 | ```typescript 83 | import ts from '@wessberg/rollup-plugin-ts'; 84 | 85 | // ... 86 | 87 | ts({ 88 | transformers: [ 89 | ({ program }) => ({ 90 | before: transformer(program), 91 | }), 92 | ], 93 | }), 94 | ``` 95 | 96 | ### 2. Option 2: Adjust your `rollup-plugin-typescript2` plugin configuration 97 | 98 | ```typescript 99 | import typescript from 'rollup-plugin-typescript2'; 100 | 101 | // ... 102 | 103 | typescript({ 104 | transformers: [ 105 | service => ({ 106 | before: [transformer(service.getProgram())], 107 | after: [], 108 | }), 109 | ], 110 | }), 111 | ``` 112 | 113 | 114 | ## TTypeScript 115 | 116 | [See example here](https://github.com/janjakubnanista/ts-type-checked/tree/main/examples/ttypescript) 117 | 118 | ### 1. Install `ttypescript` 119 | 120 | ```bash 121 | # NPM 122 | npm install --dev ttypescript 123 | 124 | # Yarn 125 | yarn add -D ttypescript 126 | ``` 127 | 128 | ### 2. Add `ts-type-checked` transformer 129 | 130 | In order to enable `ts-type-checked` in your TTypescript project you need to configure plugins in your `tsconfig.json`. 131 | 132 | ```json 133 | { 134 | "compilerOptions": { 135 | "plugins": [ 136 | { "transform": "ts-type-checked/transformer" } 137 | ] 138 | } 139 | } 140 | ``` 141 | 142 | 143 | ## Jest 144 | 145 | [See example here](https://github.com/janjakubnanista/ts-type-checked/tree/main/examples/jest) 146 | 147 | In order to enable `ts-type-checked` in your Jest tests you need to switch to `ttypescript` compiler. 148 | 149 | ### 1. Configure `ttypescript` 150 | 151 | See [the instructions above](#installation--ttypescript). 152 | 153 | ### 2. Set `ttypescript` as your compiler 154 | 155 | In your `jest.config.js` (or `package.json`): 156 | 157 | ```javascript 158 | module.exports = { 159 | preset: 'ts-jest', 160 | globals: { 161 | 'ts-jest': { 162 | compiler: 'ttypescript', 163 | }, 164 | }, 165 | }; 166 | ``` 167 | 168 | 169 | ## Mocha 170 | 171 | [See example here](https://github.com/janjakubnanista/ts-type-checked/tree/main/examples/mocha) 172 | 173 | In order to enable `ts-type-checked` in your Jest tests you need to switch to `ttypescript` compiler. 174 | 175 | ### 1. Configure `ttypescript` 176 | 177 | See [the instructions above](#installation--ttypescript). 178 | 179 | ### 2. Set `ttypescript` as your compiler 180 | 181 | In your `mocha.setup.js` (or the place where you are registering `ts-node` for `mocha`): 182 | 183 | ```javascript 184 | require('ts-node').register({ 185 | compiler: 'ttypescript', 186 | project: './tsconfig.json', 187 | }); 188 | ``` 189 | 190 | 191 | ## ts-node 192 | 193 | [See example here](https://github.com/janjakubnanista/ts-type-checked/tree/main/examples/ts-node) 194 | 195 | ### 1. Configure `ttypescript` 196 | 197 | See [the instructions above](#installation--ttypescript). 198 | 199 | ### 2. Set `ttypescript` as your compiler 200 | 201 | Either using command line: 202 | 203 | ```bash 204 | $ ts-node --compiler ttypescript ... 205 | ``` 206 | 207 | Or the programmatic API: 208 | 209 | ```javascript 210 | require('ts-node').register({ 211 | compiler: 'ttypescript' 212 | }) 213 | ``` -------------------------------------------------------------------------------- /docs/SUPPORTED_TYPESCRIPT_VERSIONS.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | ts-type-checked 4 |

5 | 6 | < Back to project 7 | 8 | # Supported TypeScript versions 9 | 10 | `ts-type-checked` has an extensive E2E test suite found in the [test](https://github.com/janjakubnanista/ts-type-checked/tree/main/test) folder. This suite is being run against several TS versions (the list can be found [here](https://github.com/janjakubnanista/ts-type-checked/blob/main/test/scripts/versions.txt)): 11 | 12 | - `3.9.2` 13 | - `3.8.2` 14 | - `3.7.2` 15 | - `3.6.2` 16 | - `3.5.1` 17 | - `3.4.1` 18 | - `3.3.1` 19 | - `3.2.1` 20 | - `3.1.1` 21 | - `3.0.1` 22 | - `2.9.1` 23 | - `2.8.3` 24 | - `2.7.2` -------------------------------------------------------------------------------- /examples/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '../.eslintrc.js', 3 | rules: { 4 | '@typescript-eslint/ban-types': 0, 5 | '@typescript-eslint/no-explicit-any': 0, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | **/index.js -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # ts-type-checked setup examples 2 | 3 | This folder contains a `yarn workspace` based project with examples of integration with the most popular build/test tools: 4 | 5 | - `webpack` 6 | - `rollup` 7 | - `ttypescript` 8 | - `jest` (using `ttypescript`) 9 | 10 | ## `webpack` 11 | 12 | -------------------------------------------------------------------------------- /examples/index.ts: -------------------------------------------------------------------------------- 1 | import { typeCheckFor } from 'ts-type-checked'; 2 | 3 | interface User { 4 | name: string; 5 | age: number; 6 | hobbies?: string[]; 7 | } 8 | 9 | const isAString = typeCheckFor(); 10 | const isANumber = typeCheckFor(); 11 | const isAUser = typeCheckFor(); 12 | 13 | [ 14 | 1, 15 | true, 16 | undefined, 17 | null, 18 | 'Hello World!', 19 | { name: 'Joe', age: 8 }, 20 | { name: 'John', age: 'None' }, 21 | { name: 'Dough', age: 6, hobbies: 'none' }, 22 | { name: 'Jan', age: 30, hobbies: ['gardening', 'coding'] }, 23 | ].forEach((value) => { 24 | console.log(JSON.stringify(value)); // eslint-disable-line no-console 25 | console.log('\tIs a string:\t%s', isAString(value)); // eslint-disable-line no-console 26 | console.log('\tIs a number:\t%s', isANumber(value)); // eslint-disable-line no-console 27 | console.log('\tIs a User:\t%s', isAUser(value)); // eslint-disable-line no-console 28 | }); 29 | -------------------------------------------------------------------------------- /examples/jest/example.spec.ts: -------------------------------------------------------------------------------- 1 | import 'jest'; 2 | import { isA, typeCheckFor } from 'ts-type-checked'; 3 | 4 | interface User { 5 | name: string; 6 | age?: number; 7 | } 8 | 9 | const isAUser = typeCheckFor(); 10 | const getTypeName = (value: unknown): string => { 11 | if (isAUser(value)) return 'User'; 12 | if (isA(value)) return 'String'; 13 | if (isA(value)) return 'Number'; 14 | 15 | return 'Unknown!!!'; 16 | }; 17 | 18 | describe('example tests', () => { 19 | test('ts-type-checked should work with jest', () => { 20 | expect(getTypeName('hey')).toBe('String'); 21 | expect(getTypeName('')).toBe('String'); 22 | 23 | expect(getTypeName(6)).toBe('Number'); 24 | expect(getTypeName(NaN)).toBe('Number'); 25 | 26 | expect(getTypeName({ name: 'John' })).toBe('User'); 27 | expect(getTypeName({ name: 'John', age: 7 })).toBe('User'); 28 | 29 | expect(getTypeName({ name: 'John', age: 'Not-a-number' })).toBe('Unknown!!!'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /examples/jest/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | globals: { 4 | 'ts-jest': { 5 | compiler: 'ttypescript', 6 | }, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /examples/jest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ts-type-checked/examples--jest", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "build": "echo 'jest based example requires no building'", 7 | "clean": "echo 'jest based example requires no cleaning'", 8 | "test": "jest --clearCache && jest" 9 | }, 10 | "devDependencies": { 11 | "@types/jest": "^26.0.4", 12 | "jest": "^26.1.0", 13 | "ts-jest": "^26.1.1", 14 | "ts-loader": "^7.0.5", 15 | "ts-node": "^8.9.1", 16 | "ts-type-checked": "file:../../dist", 17 | "ttypescript": "^1.5.10", 18 | "typescript": "^3.9.6" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/jest/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "plugins": [ 5 | { "transform": "ts-type-checked/transformer" } 6 | ] 7 | } 8 | } -------------------------------------------------------------------------------- /examples/mocha/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": "mocha.setup.js", 3 | "spec": "*.spec.ts" 4 | } 5 | -------------------------------------------------------------------------------- /examples/mocha/example.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { isA, typeCheckFor } from 'ts-type-checked'; 3 | 4 | interface User { 5 | name: string; 6 | age?: number; 7 | } 8 | 9 | const isAUser = typeCheckFor(); 10 | const getTypeName = (value: unknown): string => { 11 | if (isAUser(value)) return 'User'; 12 | if (isA(value)) return 'String'; 13 | if (isA(value)) return 'Number'; 14 | 15 | return 'Unknown!!!'; 16 | }; 17 | 18 | describe('example tests', () => { 19 | it('should work with mocha', () => { 20 | expect(getTypeName('hey')).to.equal('String'); 21 | expect(getTypeName('')).to.equal('String'); 22 | 23 | expect(getTypeName(6)).to.equal('Number'); 24 | expect(getTypeName(NaN)).to.equal('Number'); 25 | 26 | expect(getTypeName({ name: 'John' })).to.equal('User'); 27 | expect(getTypeName({ name: 'John', age: 7 })).to.equal('User'); 28 | 29 | expect(getTypeName({ name: 'John', age: 'Not-a-number' })).to.equal('Unknown!!!'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /examples/mocha/mocha.setup.js: -------------------------------------------------------------------------------- 1 | require('ts-node').register({ 2 | compiler: 'ttypescript', 3 | project: './tsconfig.json', 4 | }); 5 | -------------------------------------------------------------------------------- /examples/mocha/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ts-type-checked-examples/mocha", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "echo 'mocha based example requires no building'", 7 | "clean": "echo 'mocha based example requires no cleaning'", 8 | "test": "mocha" 9 | }, 10 | "devDependencies": { 11 | "@types/chai": "^4.2.11", 12 | "@types/mocha": "^7.0.2", 13 | "chai": "^4.2.0", 14 | "fast-check": "^1.25.1", 15 | "mocha": "8.0.1", 16 | "ts-node": "8.10.2", 17 | "ts-type-checked": "file:../../dist", 18 | "ttypescript": "^1.5.10", 19 | "typescript": "^3.9.6" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/mocha/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": false, 5 | "plugins": [ 6 | { "transform": "ts-type-checked/transformer" } 7 | ] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ts-type-checked/examples", 3 | "private": true, 4 | "version": "0.5.0", 5 | "scripts": { 6 | "build": "yarn workspaces run build", 7 | "clean": "rm -rf index.js && yarn workspaces run clean", 8 | "test": "yarn workspaces run test" 9 | }, 10 | "workspaces": [ 11 | "jest", 12 | "mocha", 13 | "rollup", 14 | "ts-node", 15 | "ttypescript", 16 | "webpack" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /examples/rollup/index.ts: -------------------------------------------------------------------------------- 1 | import '../index'; 2 | -------------------------------------------------------------------------------- /examples/rollup/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ts-type-checked-examples/rollup", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "rollup -c", 7 | "clean": "rm -rf index.js", 8 | "start": "node index.js", 9 | "test": "yarn build && yarn start" 10 | }, 11 | "devDependencies": { 12 | "@wessberg/rollup-plugin-ts": "^1.2.24", 13 | "rollup": "^2.6.1", 14 | "rollup-plugin-node-resolve": "^5.2.0", 15 | "rollup-plugin-typescript2": "^0.27.0", 16 | "ts-type-checked": "file:../../dist", 17 | "typescript": "^3.9.6" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/rollup/rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve'; 2 | import ts from '@wessberg/rollup-plugin-ts'; 3 | // import typescript from 'rollup-plugin-typescript2'; 4 | import transformer from 'ts-type-checked/transformer'; 5 | 6 | export default { 7 | input: './index.ts', 8 | output: { 9 | file: 'index.js', 10 | format: 'iife', 11 | }, 12 | plugins: [ 13 | resolve(), 14 | // Uncomment these lines if you are using rollup-plugin-typescript2 15 | // (Don't forget to comment the @wessberg/rollup-plugin-ts below) 16 | // 17 | // typescript({ 18 | // transformers: [ 19 | // service => ({ 20 | // before: [transformer(service.getProgram())], 21 | // after: [], 22 | // }), 23 | // ], 24 | // }), 25 | 26 | // I prefer @wessberg's rollup-plugin-ts myself but feel free to comment this bit out 27 | // and uncomment the bit above 28 | ts({ 29 | transformers: [ 30 | ({ program }) => ({ 31 | before: transformer(program), 32 | }), 33 | ], 34 | }), 35 | ], 36 | }; 37 | -------------------------------------------------------------------------------- /examples/rollup/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json" 3 | } -------------------------------------------------------------------------------- /examples/ts-node/index.ts: -------------------------------------------------------------------------------- 1 | import '../index'; 2 | -------------------------------------------------------------------------------- /examples/ts-node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ts-type-checked-examples/ts-node", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "ttsc", 7 | "clean": "rm -rf index.js", 8 | "test": "ts-node --compiler ttypescript index.ts" 9 | }, 10 | "devDependencies": { 11 | "ts-type-checked": "file:../../dist", 12 | "ttypescript": "^1.5.10", 13 | "typescript": "^3.9.6" 14 | }, 15 | "dependencies": { 16 | "ts-node": "^8.10.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/ts-node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": false, 5 | "plugins": [ 6 | { "transform": "ts-type-checked/transformer" } 7 | ] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "emitDecoratorMetadata": true, 5 | "esModuleInterop": true, 6 | "experimentalDecorators": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "importHelpers": true, 9 | "noEmit": false, 10 | "noErrorTruncation": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "noImplicitAny": true, 13 | "noImplicitThis": false, 14 | "noImplicitReturns": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "preserveConstEnums": true, 18 | "resolveJsonModule": true, 19 | "removeComments": false, 20 | "skipLibCheck": true, 21 | "sourceMap": true, 22 | "strict": true, 23 | "strictFunctionTypes": true, 24 | "strictBindCallApply": true, 25 | "strictNullChecks": true, 26 | "strictPropertyInitialization": true, 27 | "suppressImplicitAnyIndexErrors": false, 28 | "moduleResolution": "node", 29 | "target": "es5", 30 | "module": "commonjs", 31 | "lib": [ 32 | "es2019" 33 | ] 34 | }, 35 | "exclude": ["node_modules"] 36 | } -------------------------------------------------------------------------------- /examples/ttypescript/index.ts: -------------------------------------------------------------------------------- 1 | import '../index'; 2 | -------------------------------------------------------------------------------- /examples/ttypescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ts-type-checked-examples/ttypescript", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "ttsc", 7 | "clean": "rm -rf index.js", 8 | "start": "node index.js", 9 | "test": "yarn build && yarn start" 10 | }, 11 | "devDependencies": { 12 | "ts-type-checked": "file:../../dist", 13 | "ttypescript": "^1.5.10", 14 | "typescript": "^3.9.6" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/ttypescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": false, 5 | "plugins": [ 6 | { "transform": "ts-type-checked/transformer" } 7 | ] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/webpack/index.ts: -------------------------------------------------------------------------------- 1 | import '../index'; 2 | -------------------------------------------------------------------------------- /examples/webpack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ts-type-checked-examples/webpack", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "build": "webpack", 7 | "clean": "rm -rf index.js", 8 | "start": "node index.js", 9 | "test": "yarn build && yarn start" 10 | }, 11 | "devDependencies": { 12 | "ts-loader": "^7.0.5", 13 | "ts-type-checked": "file:../../dist", 14 | "webpack": "^4.42.1", 15 | "webpack-cli": "^3.3.11" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/webpack/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json" 3 | } -------------------------------------------------------------------------------- /examples/webpack/webpack.config.js: -------------------------------------------------------------------------------- 1 | const transformer = require('ts-type-checked/transformer'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: './index.ts', 6 | output: { 7 | filename: `index.js`, 8 | path: __dirname, 9 | }, 10 | resolve: { 11 | extensions: ['.ts', '.js'], 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.ts$/, 17 | // awesome-typescript-loader works just as good 18 | loader: 'ts-loader', 19 | options: { 20 | getCustomTransformers: (program) => ({ 21 | before: [transformer(program)], 22 | }), 23 | }, 24 | }, 25 | ], 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-type-checked", 3 | "version": "0.6.5", 4 | "description": "Type checking utilities for TypeScript.", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/janjakubnanista/ts-type-checked.git" 8 | }, 9 | "keywords": [ 10 | "TypeScript", 11 | "type checking", 12 | "type checks", 13 | "duck typing", 14 | "type guards" 15 | ], 16 | "author": "Jan Jakub Nanista ", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/janjakubnanista/ts-type-checked/issues" 20 | }, 21 | "homepage": "https://github.com/janjakubnanista/ts-type-checked#readme", 22 | "devDependencies": { 23 | "@rollup/plugin-node-resolve": "^8.1.0", 24 | "@types/node": "^14.0.18", 25 | "@typescript-eslint/eslint-plugin": "^3.6.0", 26 | "@typescript-eslint/parser": "^3.6.0", 27 | "@wessberg/rollup-plugin-ts": "^1.2.27", 28 | "eslint": "^7.4.0", 29 | "eslint-config-prettier": "^6.11.0", 30 | "eslint-plugin-prettier": "^3.1.4", 31 | "eslint-plugin-react": "^7.20.3", 32 | "eslint-plugin-sort-imports-es6-autofix": "^0.5.0", 33 | "husky": "^4.2.5", 34 | "lint-staged": "^10.2.11", 35 | "prettier": "^2.0.5", 36 | "rollup": "^2.20.0", 37 | "rollup-plugin-copy": "^3.3.0", 38 | "typescript": "^3.9.6" 39 | }, 40 | "husky": { 41 | "hooks": { 42 | "pre-commit": "lint-staged" 43 | } 44 | }, 45 | "lint-staged": { 46 | "*.{js,ts}(x)?": [ 47 | "eslint --fix" 48 | ] 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /res/ts-type-checked.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janjakubnanista/ts-type-checked/7c1a7a4aeefa46dbf52c98c8a1dbd7b86dc734ca/res/ts-type-checked.jpg -------------------------------------------------------------------------------- /res/ts-type-checked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janjakubnanista/ts-type-checked/7c1a7a4aeefa46dbf52c98c8a1dbd7b86dc734ca/res/ts-type-checked.png -------------------------------------------------------------------------------- /res/ts-type-checked@xs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janjakubnanista/ts-type-checked/7c1a7a4aeefa46dbf52c98c8a1dbd7b86dc734ca/res/ts-type-checked@xs.jpg -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import copy from 'rollup-plugin-copy'; 2 | import resolve from '@rollup/plugin-node-resolve'; 3 | import ts from '@wessberg/rollup-plugin-ts'; 4 | 5 | const defaults = { 6 | external: ['child_process', 'path', 'typescript'], 7 | plugins: [ 8 | resolve({ preferBuiltins: true }), 9 | ts(), 10 | copy({ 11 | targets: [{ src: ['package.json', 'LICENSE', 'README.md'], dest: './dist' }], 12 | }), 13 | ], 14 | }; 15 | 16 | export default [ 17 | { 18 | input: './src/index.ts', 19 | output: { 20 | file: './dist/index.js', 21 | format: 'cjs', 22 | }, 23 | ...defaults, 24 | }, 25 | { 26 | input: './src/transformer/index.ts', 27 | output: { 28 | file: './dist/transformer.js', 29 | format: 'cjs', 30 | }, 31 | ...defaults, 32 | }, 33 | { 34 | input: './src/runtime.ts', 35 | output: { 36 | file: './dist/runtime.js', 37 | format: 'cjs', 38 | }, 39 | ...defaults, 40 | }, 41 | ]; 42 | -------------------------------------------------------------------------------- /scripts/create-version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | VERSION=$1 4 | if [ -z "$VERSION" ]; then 5 | echo "Please provide version as the first argument" 6 | exit 1 7 | fi 8 | 9 | yarn version --strict-semver --new-version "$VERSION" -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a type guard function for type T. 3 | * See {@link isA} for the signature. 4 | * 5 | * This works great for when you need to pass the type guard 6 | * as e.g. an argument to a filter function: 7 | * 8 | * @example 9 | * ``` 10 | * const users = objects.filter(typeCheckerFor()); 11 | * ``` 12 | * 13 | * Which is an equivalent of writing: 14 | * 15 | * @example 16 | * ``` 17 | * const users = objects.filter(value => isA(value)); 18 | * ``` 19 | * 20 | * @example 21 | * ``` 22 | * const isAString = typeCheckFor(); 23 | * const isANumericArray = typeCheckFor(); 24 | * 25 | * isAString(''); // true 26 | * isAString('something'); // true 27 | * isAString(7); // false 28 | * isAString({}); // false 29 | * 30 | * isANumericArray([]); // true 31 | * isANumericArray([1, NaN, 0.89]); // true 32 | * isANumericArray(['string']); // true 33 | * isANumericArray({}); // false 34 | * 35 | * type Color = 'red' | 'yellow' | 'blue'; 36 | * const isAColor = typeCheckFor(); 37 | * ``` 38 | * 39 | * @function 40 | * @template T 41 | * @return {function(value: unknown): void} True if {@param value} is assignable to type T 42 | */ 43 | export declare function typeCheckFor(): (value: unknown) => value is T; 44 | 45 | /** 46 | * Type guard function for type T (checks whether {@param value} is of type T). 47 | * 48 | * This is the quicker way of creating a type guard and works great 49 | * for when you need to do the check in e.g. a conditional: 50 | * 51 | * @example 52 | * ``` 53 | * if (isA(someValue)) { 54 | * // someValue is a boolean array 55 | * } 56 | * ``` 57 | * 58 | * @function 59 | * @template T 60 | * @param {unknown} value - The value to check 61 | * @return {boolean} True if {@param value} is assignable to type T 62 | */ 63 | export declare function isA(value: unknown): value is T; 64 | 65 | // If someone forgets to register ts-type-checked/transformer then tsc 66 | // is going to actually import this file which will throw this error 67 | // for easier problem solving 68 | throw new Error( 69 | 'It looks like you have forgotten to register the transform for ts-type-checked!\n\nPlease look at the installation guide to see how to do that for your project:\n\nhttps://www.npmjs.com/package/ts-type-checked#installation', 70 | ); 71 | -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-type-checked", 3 | "version": "0.5.0", 4 | "description": "Type checking utilities for TypeScript.", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/janjakubnanista/ts-type-checked.git" 10 | }, 11 | "keywords": [ 12 | "TypeScript", 13 | "type checking", 14 | "type checks", 15 | "duck typing", 16 | "type guards" 17 | ], 18 | "author": "Jan Jakub Nanista ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/janjakubnanista/ts-type-checked/issues" 22 | }, 23 | "homepage": "https://github.com/janjakubnanista/ts-type-checked#readme", 24 | "devDependencies": { 25 | "@rollup/plugin-node-resolve": "^8.1.0", 26 | "@wessberg/rollup-plugin-ts": "^1.2.27", 27 | "rollup": "^2.20.0", 28 | "typescript": "^3.9.6" 29 | }, 30 | "peerDependencies": { 31 | "typescript": ">=2.7.2" 32 | }, 33 | "files": [ 34 | "index.js", 35 | "index.d.ts", 36 | "transformer.js", 37 | "transformer.d.ts" 38 | ], 39 | "scripts": { 40 | "build": "yarn build:compile && yarn build:lint", 41 | "build:compile": "rollup -c", 42 | "build:lint": "eslint . --fix --ext .js,.d.ts", 43 | "clean": "find ./src -type f \\( -name '*js' -or -name '*d.ts' \\) -not \\( -name 'rollup.config.js' \\) -delete", 44 | "test": "echo 'No test command defined'", 45 | "watch": "rollup -cw" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/runtime.ts: -------------------------------------------------------------------------------- 1 | type TypeGuard = (value: unknown) => value is T; 2 | 3 | /** 4 | * Helper utility that wraps a type guard with circular reference check. 5 | * 6 | * To illustrate the function of this cycle breaker, let's take a simple recursive type: 7 | * 8 | * @example 9 | * ``` 10 | * interface Recursive { 11 | * parent?: Recursive; 12 | * } 13 | * ``` 14 | * 15 | * We can now create an object that matches that type but has a cycle: 16 | * 17 | * @example 18 | * ``` 19 | * const node = {}; 20 | * const recursive: Recursive = Object.assign(node, { parent: node }); 21 | * ``` 22 | * 23 | * If we didn't wrap the original type guard, it would be spinning in circles 24 | * until the maximum call stack exceeded error was thrown. 25 | * 26 | * @param typeName {TypeName} Name of the type (to be displayed in an error message) 27 | * @param typeGuard {TypeGuard} The original type guard 28 | * @returns {TypeGuard} The wrapped type guard 29 | */ 30 | export function typeGuardCycleBreaker(typeGuard: TypeGuard): TypeGuard { 31 | // We will create a heap in this closure that will store all the values being checked by this type guard 32 | const heap: unknown[] = []; 33 | 34 | // We return a TypeGuard with signature identical to the original one 35 | return (value: unknown): value is T => { 36 | // First we check whether the value is now already being checked 37 | // 38 | // If the value is in the heap it means that this type guard has called itself 39 | // with the same value. That means that the result of a type check would depend on the result of the type check 40 | // and a loop would be created 41 | const heapIndex: number = heap.indexOf(value); 42 | if (heapIndex !== -1) { 43 | // Since the heap is shared between the calls we need to clear it before 44 | // throwing an error. If we didn't, following calls to the type guard 45 | // might see the stale values in the heap. This is only a problem in a very specific 46 | // scenario: 47 | // 48 | // - Try checking a circular structure with the type guard 49 | // - Catch the exception 50 | // - Mutate one value in the circular structure (break the cycle) 51 | // - Try checking the structure again 52 | heap.splice(0, heap.length); 53 | 54 | throw new Error(`Value that was passed to ts-type-checked contains a circular reference and cannot be checked`); 55 | } 56 | 57 | // If the value was not in the heap then let's add it there 58 | heap.push(value); 59 | 60 | // Now we perform the type check 61 | const isOfTypeT: boolean = typeGuard(value); 62 | 63 | // And get the value out of the heap 64 | heap.pop(); 65 | 66 | return isOfTypeT; 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /src/transformer/index.ts: -------------------------------------------------------------------------------- 1 | import { HashTypeGuardRegistry } from './storage/HashTypeGuardRegistry'; 2 | import { MapTypeDescriptorRegistry } from './storage/MapTypeDescriptorRegistry'; 3 | import { TransformerOptions, defaultTransformerOptions } from './utils/transformerOptions'; 4 | import { TypeDescriptorRegistry, TypeGuardRegistry } from './types'; 5 | import { createDebugTypeNameGenerator } from './typeName/debugTypeNameGenerator'; 6 | import { createLogger } from './utils/logger'; 7 | import { createProductionTypeNameGenerator } from './typeName/productionTypeNameGenerator'; 8 | import { createTypeCheckVisitor } from './visitor/typeCheckVisitor'; 9 | import { createTypeDescriptorGenerator } from './typeDescriptor/typeDescriptorGenerator'; 10 | import { createTypeGuardGenerator } from './typeGuard/typeGuardGenerator'; 11 | import { createTypeGuardResolver } from './typeGuard/typeGuardResolver'; 12 | import { createTypeNameResolver } from './typeName/typeNameResolver'; 13 | import { transformUsingVisitor } from './utils/transformUsingVisitor'; 14 | import ts from 'typescript'; 15 | 16 | /** 17 | * The main transformer function. 18 | * 19 | * This needs to be registered as a TypeScript "before" transform 20 | * in your build/test configuration. 21 | * 22 | * See https://www.npmjs.com/package/ts-type-checked#installation for more information 23 | * 24 | * @param program {ts.Program} An instance of TypeScript Program 25 | * @param options {Partial} Transformer options object 26 | */ 27 | export default ( 28 | program: ts.Program, 29 | options: Partial = {}, 30 | ): ts.TransformerFactory => { 31 | const resolvedOptions: TransformerOptions = { ...defaultTransformerOptions, ...options }; 32 | const { mode, logLevel } = resolvedOptions; 33 | 34 | // Without strict null checks on we need to 35 | const compilerOptions = program.getCompilerOptions(); 36 | const strictNullChecks = 37 | compilerOptions.strictNullChecks === undefined ? !!compilerOptions.strict : !!compilerOptions.strictNullChecks; 38 | 39 | // Get a reference to a TypeScript TypeChecker in order to resolve types from type nodes 40 | const typeChecker = program.getTypeChecker(); 41 | 42 | return (context: ts.TransformationContext) => (file: ts.SourceFile) => { 43 | // Create a file specific logger 44 | const logger = createLogger(logLevel, `[${file.fileName}]`); 45 | 46 | // We will need a utility that can generate unique type names 47 | // that can be used to reference type descriptors and type guards. 48 | // 49 | // Since we are doing the transformation on per-file basis we will create 50 | // it here so that the type names are unique within one file but not necessarily 51 | // unique across the codebase 52 | const typeNameGenerator = 53 | mode === 'development' ? createDebugTypeNameGenerator(typeChecker) : createProductionTypeNameGenerator(); 54 | 55 | // Now we need to walk the AST and replace all the references to isA or typeCheckFor 56 | // with generated type guards. 57 | // 58 | // Simple type guards (string, number etc.) will be inlined since they translate 59 | // into something like 60 | // 61 | // value => typeof value === 'string' 62 | // 63 | // But the interface type guards could contain cycles (on the type level, e.g.) 64 | // 65 | // interface Node { 66 | // parent?: Node; 67 | // } 68 | // 69 | // If we inlined these the code generator would be spinning in cycles. Therefore 70 | // we need to store them somewhere to be able to access them by the unique type id 71 | // so that they can call themselves recursively. 72 | // 73 | // The solution is to create a runtime variable, a hash with type names 74 | // as keys and type guards as values, let's call it type guard map: 75 | const typeGuardRegistryIdentifier = ts.createIdentifier('___isA___'); 76 | const typeGuardRegistry: TypeGuardRegistry = new HashTypeGuardRegistry(typeGuardRegistryIdentifier); 77 | 78 | const typeDescriptorRegistry: TypeDescriptorRegistry = new MapTypeDescriptorRegistry(typeNameGenerator); 79 | 80 | const typeGuardGenerator = createTypeGuardGenerator(typeGuardRegistry, typeDescriptorRegistry, strictNullChecks); 81 | const typeDescriptorCreator = createTypeDescriptorGenerator(program, logger); 82 | const typeNameResolver = createTypeNameResolver(typeDescriptorRegistry, typeDescriptorCreator); 83 | const typeGuardResolver = createTypeGuardResolver(program, typeNameResolver, typeGuardGenerator); 84 | 85 | // Now we need an ASTVisitor function that will replace all occurrences 86 | // of isA and typeCheckFor with the generated type guards 87 | const visitor = createTypeCheckVisitor(typeChecker, typeGuardResolver); 88 | 89 | // We use the visitor to transform the file 90 | const transformedFile = transformUsingVisitor(file, context, visitor); 91 | 92 | // The last step is to insert the type guard hash into the source file 93 | return ts.updateSourceFileNode(transformedFile, [...typeGuardRegistry.code(), ...transformedFile.statements]); 94 | }; 95 | }; 96 | -------------------------------------------------------------------------------- /src/transformer/storage/HashTypeGuardRegistry.ts: -------------------------------------------------------------------------------- 1 | import { ExpressionTransformer, TypeGuardRegistry } from '../types'; 2 | import { TypeName } from '../types'; 3 | import { 4 | createElementAccess, 5 | createObjectWithProperties, 6 | createRequire, 7 | createSingleParameterFunction, 8 | createVariable, 9 | } from '../utils/codeGenerators'; 10 | import ts from 'typescript'; 11 | 12 | /** 13 | * Hash (as in plain JavaScript object) based implementation of TypeGuardRegistry 14 | */ 15 | export class HashTypeGuardRegistry implements TypeGuardRegistry { 16 | private readonly typeGuardsBeingCreated: Set = new Set(); 17 | 18 | private readonly typeGuardFunctionsByTypeName: Map = new Map(); 19 | 20 | private readonly cyclicTypeNames: Set = new Set(); 21 | 22 | constructor(private readonly identifier: ts.Identifier) {} 23 | 24 | public get(typeName: TypeName): ts.Expression | undefined { 25 | // First we check that a type under this name has been registered 26 | if (!this.typeGuardFunctionsByTypeName.has(typeName)) return undefined; 27 | 28 | // Then we create a runtime reference to the type guard 29 | return createElementAccess(this.identifier, typeName); 30 | } 31 | 32 | public create(typeName: TypeName, factory: ExpressionTransformer): ts.Expression { 33 | // Get an existing type guard function expression 34 | const hasTypeGuard = this.typeGuardFunctionsByTypeName.has(typeName); 35 | 36 | // Check whether we are not in progress of creating the very same type guard 37 | // 38 | // This can happen when types reference themselves circularly, for example: 39 | // 40 | // interface MyInterface { 41 | // parent: MyInterface | undefined; 42 | // } 43 | // 44 | const typeGuardIsBeingCreated = this.typeGuardsBeingCreated.has(typeName); 45 | 46 | // If the type guard with the same name is being created it means 47 | // that the type is cyclic so we mark it as such 48 | if (typeGuardIsBeingCreated) { 49 | this.cyclicTypeNames.add(typeName); 50 | } 51 | 52 | // If we don't have the type guard yet and we are not creating one at the moment 53 | // we need to create it 54 | if (!hasTypeGuard && !typeGuardIsBeingCreated) { 55 | // First let's mark the fact that we are creating it now 56 | this.typeGuardsBeingCreated.add(typeName); 57 | 58 | // Then create the type guard and store it in the map 59 | this.typeGuardFunctionsByTypeName.set(typeName, createSingleParameterFunction(factory)); 60 | 61 | // Finally let's remove the mark 62 | this.typeGuardsBeingCreated.delete(typeName); 63 | } 64 | 65 | // We return an expression that points to the type guard function 66 | return createElementAccess(this.identifier, typeName); 67 | } 68 | 69 | public code(): ts.Statement[] { 70 | // There is no need for a object if it is not used in the code 71 | if (this.typeGuardFunctionsByTypeName.size === 0) return []; 72 | 73 | // We will use this to remember whether there were any cyclic types 74 | let areAnyTypesCyclic = false; 75 | 76 | // In order not to create the cycle breaker in every file we import it from our module 77 | const wrapperIdentifier = ts.createIdentifier('__typeGuardCycleBreaker__'); 78 | const wrapperImport = createRequire(wrapperIdentifier, 'ts-type-checked/runtime', 'typeGuardCycleBreaker'); 79 | const wrapTypeGuard: ExpressionTransformer = (typeGuard: ts.Expression): ts.Expression => 80 | ts.createCall(wrapperIdentifier, undefined, [typeGuard]); 81 | 82 | // Now we create an object literal with all the type guards keyed by type names 83 | const typeGuardEntries = Array.from(this.typeGuardFunctionsByTypeName.entries()); 84 | const properties: ts.PropertyAssignment[] = typeGuardEntries.map(([typeName, typeGuard]) => { 85 | // First we check whether the type has been marked as cyclic 86 | const isCyclic: boolean = this.cyclicTypeNames.has(typeName); 87 | 88 | // Make sure we remember meeting any cyclic types 89 | areAnyTypesCyclic = areAnyTypesCyclic || isCyclic; 90 | 91 | // If the type is not cyclic we output a simple property assignment, 92 | // if it is cyclic we need to wrap the type guard in a cycle-breaking code 93 | const wrappedTypeGuard = isCyclic ? wrapTypeGuard(typeGuard) : typeGuard; 94 | 95 | return ts.createPropertyAssignment(ts.createLiteral(typeName), wrappedTypeGuard); 96 | }); 97 | 98 | return [ 99 | // If there were any cyclic types we will need to import the cycle breaker 100 | ...(areAnyTypesCyclic ? [wrapperImport] : []), 101 | createVariable(this.identifier, createObjectWithProperties(properties)), 102 | ]; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/transformer/storage/MapTypeDescriptorRegistry.ts: -------------------------------------------------------------------------------- 1 | import { TypeDescriptor, TypeName } from '../types'; 2 | import { TypeDescriptorRegistry, TypeNameGenerator } from '../types'; 3 | import ts from 'typescript'; 4 | 5 | /** 6 | * Map based implementation of TypeDescriptorRegistry 7 | */ 8 | export class MapTypeDescriptorRegistry implements TypeDescriptorRegistry { 9 | // Type to TypeName map 10 | private readonly typeNamesByType: Map = new Map(); 11 | 12 | // TypeName to TypeDescriptor map 13 | private readonly typeDescriptorsByTypeName: Map = new Map(); 14 | 15 | // We will also need a TypeNameGenerator to create names for types 16 | constructor(private readonly typeNameGenerator: TypeNameGenerator) {} 17 | 18 | public get(typeName: TypeName): TypeDescriptor | undefined { 19 | // Simply get the type descriptor from the map 20 | return this.typeDescriptorsByTypeName.get(typeName); 21 | } 22 | 23 | public create(type: ts.Type, factory: () => TypeDescriptor): TypeName { 24 | // First check whether we already have a type name for this type. 25 | const existingTypeName = this.typeNamesByType.get(type); 26 | if (existingTypeName !== undefined) return existingTypeName; 27 | 28 | // Generate a name for this type 29 | const typeName = this.typeNameGenerator(type); 30 | 31 | // First we store the name in the map which effectively "marks" the type as resolved 32 | this.typeNamesByType.set(type, typeName); 33 | 34 | // Only then we create the type descriptor. This is important since if we didn't 35 | // store the typeName in typeNamesByType prior to creating the descriptor 36 | // we might end up spinning in type reference circles 37 | this.typeDescriptorsByTypeName.set(typeName, factory()); 38 | 39 | return typeName; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/transformer/typeDescriptor/utils/assert.ts: -------------------------------------------------------------------------------- 1 | import { LibraryTypeDescriptorName } from './getLibraryTypeDescriptorName'; 2 | import ts from 'typescript'; 3 | 4 | export const isBigInt = (type: ts.Type, libraryDescriptorName?: LibraryTypeDescriptorName): boolean => 5 | !!(type.flags & ts.TypeFlags.BigInt) || libraryDescriptorName === 'BigInt'; 6 | 7 | export const isBoolean = (type: ts.Type, libraryDescriptorName?: LibraryTypeDescriptorName): boolean => 8 | !!(type.flags & ts.TypeFlags.Boolean) || libraryDescriptorName === 'Boolean'; 9 | 10 | export const isString = (type: ts.Type, libraryDescriptorName?: LibraryTypeDescriptorName): boolean => 11 | !!(type.flags & ts.TypeFlags.String) || libraryDescriptorName === 'String'; 12 | 13 | export const isNumber = (type: ts.Type, libraryDescriptorName?: LibraryTypeDescriptorName): boolean => 14 | !!(type.flags & ts.TypeFlags.Number) || libraryDescriptorName === 'Number'; 15 | 16 | export const isSymbol = (type: ts.Type, libraryDescriptorName?: LibraryTypeDescriptorName): boolean => 17 | !!(type.flags & ts.TypeFlags.ESSymbol) || libraryDescriptorName === 'Symbol'; 18 | 19 | export const isDate = ( 20 | type: ts.Type, 21 | libraryDescriptorName?: LibraryTypeDescriptorName, 22 | ): libraryDescriptorName is LibraryTypeDescriptorName => libraryDescriptorName === 'Date'; 23 | 24 | export const isMap = (type: ts.Type, libraryDescriptorName?: LibraryTypeDescriptorName): type is ts.TypeReference => 25 | libraryDescriptorName === 'Map'; 26 | 27 | export const isSet = (type: ts.Type, libraryDescriptorName?: LibraryTypeDescriptorName): type is ts.TypeReference => 28 | libraryDescriptorName === 'Set'; 29 | 30 | export const isPromise = (type: ts.Type, libraryDescriptorName?: LibraryTypeDescriptorName): boolean => 31 | libraryDescriptorName === 'Promise'; 32 | 33 | export const isRegExp = ( 34 | type: ts.Type, 35 | libraryDescriptorName?: LibraryTypeDescriptorName, 36 | ): libraryDescriptorName is LibraryTypeDescriptorName => libraryDescriptorName === 'RegExp'; 37 | 38 | export const isInterface = (type: ts.Type, libraryDescriptorName?: LibraryTypeDescriptorName): boolean => 39 | !!(type.flags & ts.TypeFlags.Object) || libraryDescriptorName === 'Object'; 40 | 41 | export const isLiteral = (type: ts.Type): type is ts.LiteralType => 42 | (typeof type.isLiteral === 'function' && type.isLiteral()) || 43 | !!(type.flags & ts.TypeFlags.BigIntLiteral) || 44 | !!(type.flags & ts.TypeFlags.Literal); 45 | 46 | export const isNull = (type: ts.Type): boolean => !!(type.flags & ts.TypeFlags.Null); 47 | 48 | export const isUndefined = (type: ts.Type): boolean => 49 | !!(type.flags & ts.TypeFlags.Undefined || type.flags & ts.TypeFlags.Void); 50 | 51 | export const isAny = (type: ts.Type): boolean => !!(type.flags & ts.TypeFlags.Any || type.flags & ts.TypeFlags.Unknown); 52 | 53 | export const isNever = (type: ts.Type): boolean => !!(type.flags & ts.TypeFlags.Never); 54 | 55 | export const isObjectKeyword = (typeNode: ts.TypeNode | undefined): boolean => 56 | typeNode?.kind === ts.SyntaxKind.ObjectKeyword; 57 | 58 | export const isTrueKeyword = (typeNode: ts.TypeNode | undefined): boolean => 59 | typeNode?.kind === ts.SyntaxKind.TrueKeyword; 60 | 61 | export const isFalseKeyword = (typeNode: ts.TypeNode | undefined): boolean => 62 | typeNode?.kind === ts.SyntaxKind.FalseKeyword; 63 | 64 | export const isTuple = (type: ts.Type, typeNode: ts.TypeNode | undefined): type is ts.TupleType => 65 | typeNode?.kind === ts.SyntaxKind.TupleType; 66 | 67 | export const isIntersection = (type: ts.Type, typeNode: ts.TypeNode | undefined): type is ts.IntersectionType => 68 | (typeof type.isIntersection === 'function' && type.isIntersection()) || 69 | typeNode?.kind === ts.SyntaxKind.IntersectionType; 70 | 71 | export const isUnion = (type: ts.Type, typeNode: ts.TypeNode | undefined): type is ts.UnionType => 72 | (typeof type.isUnion === 'function' && type.isUnion()) || 73 | typeNode?.kind === ts.SyntaxKind.UnionType || 74 | !!(type.getFlags() & ts.TypeFlags.Union); 75 | 76 | export const isClassOrInterface = (type: ts.Type, typeNode?: ts.TypeNode): type is ts.InterfaceType => 77 | (typeof type.isClassOrInterface === 'function' && type.isClassOrInterface()) || 78 | typeNode?.kind === ts.SyntaxKind.InterfaceDeclaration || 79 | !!(type.flags & ts.TypeFlags.Object); 80 | 81 | export const isFunction = ( 82 | type: ts.Type, 83 | libraryDescriptorName?: LibraryTypeDescriptorName, 84 | typeNode?: ts.TypeNode, 85 | ): boolean => 86 | typeNode?.kind === ts.SyntaxKind.FunctionType || 87 | typeNode?.kind === ts.SyntaxKind.ConstructorType || 88 | libraryDescriptorName === 'Function' || 89 | !!type.getConstructSignatures()?.length || 90 | !!type.getCallSignatures()?.length; 91 | 92 | const isArrayType = (typeChecker: ts.TypeChecker, type: ts.Type): boolean => 93 | typeof (typeChecker as any).isArrayType === 'function' && !!(typeChecker as any)?.isArrayType(type); 94 | 95 | export const isArray = ( 96 | typeChecker: ts.TypeChecker, 97 | type: ts.Type, 98 | libraryDescriptorName?: LibraryTypeDescriptorName, 99 | typeNode?: ts.TypeNode, 100 | ): type is ts.TypeReference => 101 | typeNode?.kind === ts.SyntaxKind.ArrayType || libraryDescriptorName === 'Array' || isArrayType(typeChecker, type); 102 | -------------------------------------------------------------------------------- /src/transformer/typeDescriptor/utils/getDOMElementClassName.ts: -------------------------------------------------------------------------------- 1 | import { getFirstValidDeclaration } from './getFirstValidDeclaration'; 2 | import { isSourceFileDefaultLibrary } from './getLibraryTypeDescriptorName'; 3 | import ts from 'typescript'; 4 | 5 | export const getDOMElementClassName = (program: ts.Program, type: ts.Type): string | undefined => { 6 | const declaration = getFirstValidDeclaration(type.symbol?.declarations); 7 | const sourceFile = declaration?.getSourceFile(); 8 | 9 | if (!sourceFile || !isSourceFileDefaultLibrary(program, sourceFile)) return undefined; 10 | if (!sourceFile.fileName.match(/lib.dom.d.ts$/)) return undefined; 11 | if (!type.symbol?.name.match(/(Element|^Document|^Node)$/i)) return undefined; 12 | 13 | return type.symbol.name; 14 | }; 15 | -------------------------------------------------------------------------------- /src/transformer/typeDescriptor/utils/getFirstValidDeclaration.ts: -------------------------------------------------------------------------------- 1 | import ts from 'typescript'; 2 | 3 | export const getFirstValidDeclaration = (declarations: ts.Declaration[] | undefined): ts.Declaration | undefined => { 4 | return ( 5 | declarations?.find( 6 | (declaration) => !ts.isVariableDeclaration(declaration) && !ts.isFunctionDeclaration(declaration), 7 | ) || declarations?.[0] 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/transformer/typeDescriptor/utils/getLibraryTypeDescriptorName.ts: -------------------------------------------------------------------------------- 1 | import { getFirstValidDeclaration } from './getFirstValidDeclaration'; 2 | import ts from 'typescript'; 3 | 4 | export type LibraryTypeDescriptorName = 5 | | 'Array' 6 | | 'BigInt' 7 | | 'Date' 8 | | 'Number' 9 | | 'String' 10 | | 'Boolean' 11 | | 'Object' 12 | | 'Function' 13 | | 'Promise' 14 | | 'RegExp' 15 | | 'Map' 16 | | 'Set' 17 | | 'Symbol'; 18 | 19 | const typeDescriptorNameBySymbolName: Record = { 20 | Array: 'Array', 21 | ReadonlyArray: 'Array', 22 | BigInt: 'BigInt', 23 | Number: 'Number', 24 | Function: 'Function', 25 | Date: 'Date', 26 | String: 'String', 27 | Boolean: 'Boolean', 28 | Object: 'Object', 29 | Promise: 'Promise', 30 | RegExp: 'RegExp', 31 | Map: 'Map', 32 | Set: 'Set', 33 | Symbol: 'Symbol', 34 | }; 35 | 36 | export const isSourceFileDefaultLibrary = (program: ts.Program, file: ts.SourceFile): boolean => { 37 | if (program.isSourceFileDefaultLibrary(file)) return true; 38 | if (file.fileName.match(/typescript\/lib\/lib\..*\.d\.ts$/)) return true; 39 | 40 | return false; 41 | }; 42 | 43 | export const getLibraryTypeDescriptorName = ( 44 | program: ts.Program, 45 | type: ts.Type, 46 | ): LibraryTypeDescriptorName | undefined => { 47 | const declaration = getFirstValidDeclaration(type.symbol?.declarations); 48 | const sourceFile = declaration?.getSourceFile(); 49 | 50 | if (!sourceFile || !isSourceFileDefaultLibrary(program, sourceFile)) return undefined; 51 | 52 | return typeDescriptorNameBySymbolName[type.symbol?.name]; 53 | }; 54 | -------------------------------------------------------------------------------- /src/transformer/typeDescriptor/utils/getPropertyTypeDescriptors.ts: -------------------------------------------------------------------------------- 1 | import { PropertyTypeDescriptor, TypeNameResolver } from '../../types'; 2 | import { getPropertyAccessor, isPublicProperty } from '../../utils/ast'; 3 | import ts from 'typescript'; 4 | 5 | export const getPropertyTypeDescriptors = ( 6 | typeChecker: ts.TypeChecker, 7 | scope: ts.TypeNode, 8 | properties: ts.Symbol[], 9 | typeNameResolver: TypeNameResolver, 10 | ): PropertyTypeDescriptor[] => { 11 | return properties.filter(isPublicProperty).map((property) => { 12 | const propertyType = typeChecker.getTypeOfSymbolAtLocation(property, scope); 13 | const accessor: ts.Expression = getPropertyAccessor(property, typeChecker, scope); 14 | 15 | return { 16 | _type: 'property', 17 | accessor, 18 | type: typeNameResolver(scope, propertyType), 19 | }; 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /src/transformer/typeDescriptor/utils/messages.ts: -------------------------------------------------------------------------------- 1 | export const functionTypeWarning = (typeName: string): string => ` 2 | 3 | It looks like you are trying to type check a function-like value (${typeName}). 4 | Due to very nature of JavaScript it's not possible to see what the return type of a function is 5 | or what the signature of a function was. 6 | 7 | ts-type-checked can only check whether something is of type function, nothing more. Sorry :( 8 | 9 | `; 10 | 11 | export const promiseTypeWarning = (typeName: string): string => ` 12 | 13 | It looks like you are trying to type check a Promise-like value (${typeName}). 14 | Although possible, type checking Promises is discouraged in favour of wrapping the value in a new Promise: 15 | 16 | const certainlyPromise = Promise.resolve(value); 17 | 18 | Check https://stackoverflow.com/questions/27746304/how-do-i-tell-if-an-object-is-a-promise for more information. 19 | 20 | `; 21 | -------------------------------------------------------------------------------- /src/transformer/typeGuard/typeGuardAsserter.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janjakubnanista/ts-type-checked/7c1a7a4aeefa46dbf52c98c8a1dbd7b86dc734ca/src/transformer/typeGuard/typeGuardAsserter.ts -------------------------------------------------------------------------------- /src/transformer/typeGuard/typeGuardGenerator.ts: -------------------------------------------------------------------------------- 1 | import { TypeDescriptor, TypeName } from '../types'; 2 | import { TypeDescriptorRegistry, TypeGuardGenerator, TypeGuardRegistry } from '../types'; 3 | import { 4 | createArrayTypeGuard, 5 | createMapTypeGuard, 6 | createObjectTypeGuard, 7 | createSetTypeGuard, 8 | createTupleTypeGuard, 9 | } from './utils/codeGenerators'; 10 | import { 11 | createIsInstanceOf, 12 | createIsNotPrimitive, 13 | createLogicalAndChain, 14 | createLogicalOrChain, 15 | } from '../utils/codeGenerators'; 16 | import ts from 'typescript'; 17 | 18 | type StrictNullChecksPrefixer = (typeGuard: ts.Expression, value: ts.Expression) => ts.Expression; 19 | 20 | export const createTypeGuardGenerator = ( 21 | typeGuardRegistry: TypeGuardRegistry, 22 | typeDescriptorRegistry: TypeDescriptorRegistry, 23 | strictNullChecks: boolean, 24 | ): TypeGuardGenerator => { 25 | // If the strict null checks are off (which they shouldn't), 26 | // the optional types all pretend to be non-optional 27 | // so we need to make sure every type can also be null or undefined 28 | const prefixNullChecks: StrictNullChecksPrefixer = strictNullChecks 29 | ? // If strictNullChecks are on just return the original type guard 30 | (typeGuard) => typeGuard 31 | : // But if they are off prepend every type guard with check for null or undefined 32 | (typeGuard, value): ts.Expression => { 33 | return createLogicalOrChain( 34 | ts.createStrictEquality(value, ts.createNull()), 35 | ts.createStrictEquality(value, ts.createIdentifier('undefined')), 36 | typeGuard, 37 | ); 38 | }; 39 | 40 | const typeGuardGenerator: TypeGuardGenerator = (typeName: TypeName, value: ts.Expression): ts.Expression => { 41 | // Step 1: Check if type guard already exists for this type 42 | const storedTypeGuardFunction: ts.Expression | undefined = typeGuardRegistry.get(typeName); 43 | if (storedTypeGuardFunction) { 44 | return ts.createCall(storedTypeGuardFunction, [], [value]); 45 | } 46 | 47 | // Step 2: Get the TypeDescriptor for the typeName 48 | const typeDescriptor = typeDescriptorRegistry.get(typeName); 49 | if (typeDescriptor === undefined) { 50 | throw new Error(`Unable to find type descriptor for type '${typeName}'`); 51 | } 52 | 53 | // Step 3: Turn the TypeDescriptor into a type guard expression 54 | // 55 | // In this step we need to prefix all applicable type guards 56 | // with null checks bypass (see above) 57 | switch (typeDescriptor._type) { 58 | case 'literal': 59 | return prefixNullChecks(ts.createStrictEquality(value, typeDescriptor.value), value); 60 | 61 | case 'keyword': 62 | switch (typeDescriptor.value) { 63 | case 'object': 64 | return prefixNullChecks(createIsNotPrimitive(value), value); 65 | 66 | default: 67 | return prefixNullChecks( 68 | ts.createStrictEquality(ts.createTypeOf(value), ts.createLiteral(typeDescriptor.value)), 69 | value, 70 | ); 71 | } 72 | 73 | case 'intersection': 74 | const intersectionTypeCheckMethod = typeGuardRegistry.create(typeName, (value) => 75 | prefixNullChecks( 76 | createLogicalAndChain( 77 | ...typeDescriptor.types.map((typeName) => typeGuardGenerator(typeName, value)), 78 | ), 79 | value, 80 | ), 81 | ); 82 | 83 | return ts.createCall(intersectionTypeCheckMethod, undefined, [value]); 84 | 85 | case 'union': 86 | const unionTypeCheckMethod = typeGuardRegistry.create(typeName, (value) => 87 | prefixNullChecks( 88 | createLogicalOrChain( 89 | ...typeDescriptor.types.map((typeName) => typeGuardGenerator(typeName, value)), 90 | ), 91 | value, 92 | ), 93 | ); 94 | 95 | return ts.createCall(unionTypeCheckMethod, undefined, [value]); 96 | 97 | case 'array': 98 | return prefixNullChecks( 99 | createArrayTypeGuard(value, (element) => typeGuardGenerator(typeDescriptor.type, element)), 100 | value, 101 | ); 102 | 103 | case 'tuple': 104 | return prefixNullChecks( 105 | createTupleTypeGuard(value, typeDescriptor.types.length, (element, index) => { 106 | return typeGuardGenerator(typeDescriptor.types[index], element); 107 | }), 108 | value, 109 | ); 110 | 111 | case 'class': 112 | return prefixNullChecks(createIsInstanceOf(value, typeDescriptor.value), value); 113 | 114 | case 'map': 115 | return prefixNullChecks( 116 | createMapTypeGuard( 117 | value, 118 | (key) => typeGuardGenerator(typeDescriptor.keyType, key), 119 | (value) => typeGuardGenerator(typeDescriptor.valueType, value), 120 | ), 121 | value, 122 | ); 123 | 124 | case 'set': 125 | return prefixNullChecks( 126 | createSetTypeGuard(value, (element) => typeGuardGenerator(typeDescriptor.type, element)), 127 | value, 128 | ); 129 | 130 | case 'promise': 131 | const promiseTypeCheckMethod = typeGuardRegistry.create('Promise', (value) => 132 | prefixNullChecks( 133 | createObjectTypeGuard(value, { properties: typeDescriptor.properties }, typeGuardGenerator), 134 | value, 135 | ), 136 | ); 137 | 138 | return ts.createCall(promiseTypeCheckMethod, undefined, [value]); 139 | 140 | case 'function': 141 | const functionTypeCheck = ts.createStrictEquality(ts.createTypeOf(value), ts.createLiteral('function')); 142 | 143 | // If this is just a simple function with no additional properties then return the typeof check immediately 144 | if (!typeDescriptor.properties.length && !typeDescriptor.stringIndexType && !typeDescriptor.numberIndexType) { 145 | return functionTypeCheck; 146 | } 147 | 148 | // If though the function has additional properties and/or index we need to check that 149 | const functionTypeCheckMethod = typeGuardRegistry.create(typeName, (value) => 150 | prefixNullChecks( 151 | createLogicalAndChain(functionTypeCheck, createObjectTypeGuard(value, typeDescriptor, typeGuardGenerator)), 152 | value, 153 | ), 154 | ); 155 | 156 | return ts.createCall(functionTypeCheckMethod, undefined, [value]); 157 | 158 | case 'interface': 159 | const objectTypeCheckMethod = typeGuardRegistry.create(typeName, (value) => 160 | prefixNullChecks(createObjectTypeGuard(value, typeDescriptor, typeGuardGenerator), value), 161 | ); 162 | 163 | return ts.createCall(objectTypeCheckMethod, undefined, [value]); 164 | 165 | case 'unspecified': 166 | return ts.createTrue(); 167 | 168 | case 'never': 169 | return ts.createFalse(); 170 | 171 | default: 172 | throw new Error('Unable to create a checker for type descriptor ' + (typeDescriptor as TypeDescriptor)._type); 173 | } 174 | }; 175 | 176 | return typeGuardGenerator; 177 | }; 178 | -------------------------------------------------------------------------------- /src/transformer/typeGuard/typeGuardResolver.ts: -------------------------------------------------------------------------------- 1 | import { TypeGuardGenerator, TypeGuardResolver, TypeNameResolver } from '../types'; 2 | import ts from 'typescript'; 3 | 4 | export const createTypeGuardResolver = ( 5 | program: ts.Program, 6 | typeNameResolver: TypeNameResolver, 7 | typeGuardGenerator: TypeGuardGenerator, 8 | ): TypeGuardResolver => { 9 | const typeChecker = program.getTypeChecker(); 10 | 11 | const typeGuardResolver = (typeNode: ts.TypeNode, value: ts.Expression): ts.Expression => { 12 | // Step 1: Get type from TypeNode 13 | const type = typeChecker.getTypeFromTypeNode(typeNode); 14 | 15 | // Step 2: Resolve the type descriptor 16 | const typeName = typeNameResolver(typeNode, type); 17 | 18 | // Step 3: Turn the type descriptor into a type guard 19 | const typeGuard = typeGuardGenerator(typeName, value); 20 | 21 | // Step 4: RETURN!!! 22 | return typeGuard; 23 | }; 24 | 25 | return typeGuardResolver; 26 | }; 27 | -------------------------------------------------------------------------------- /src/transformer/typeGuard/utils/codeGenerators.ts: -------------------------------------------------------------------------------- 1 | import { ExpressionTransformer, ObjectTypeDescriptor, TypeGuardGenerator } from '../../types'; 2 | import { 3 | createArrayEvery, 4 | createArrayFrom, 5 | createIsArray, 6 | createIsInstanceOf, 7 | createIsNotNullOrUndefined, 8 | createIsNotPrimitive, 9 | createLogicalAndChain, 10 | createLogicalOrChain, 11 | createObjectKeys, 12 | } from '../../utils/codeGenerators'; 13 | import ts from 'typescript'; 14 | 15 | export const createArrayTypeGuard = (value: ts.Expression, createElementCheck: ExpressionTransformer): ts.Expression => 16 | createLogicalAndChain(createIsArray(value), createArrayEvery(value, createElementCheck)); 17 | 18 | export const createSetTypeGuard = (value: ts.Expression, createElementCheck: ExpressionTransformer): ts.Expression => { 19 | const setValues = ts.createCall(ts.createPropertyAccess(value, 'values'), [], []); 20 | const setValuesAsArray = createArrayFrom(setValues); 21 | const elementChecks = createArrayEvery(setValuesAsArray, createElementCheck); 22 | 23 | return createLogicalAndChain(createIsInstanceOf(value, ts.createIdentifier('Set')), elementChecks); 24 | }; 25 | 26 | export const createMapTypeGuard = ( 27 | value: ts.Expression, 28 | createKeyCheck: ExpressionTransformer, 29 | createValueCheck: ExpressionTransformer, 30 | ): ts.Expression => { 31 | const mapEntries = ts.createCall(ts.createPropertyAccess(value, 'entries'), [], []); 32 | const mapEntriesAsArray = createArrayFrom(mapEntries); 33 | 34 | const entryChecks = createArrayEvery( 35 | mapEntriesAsArray, 36 | (entry) => 37 | createLogicalAndChain( 38 | createKeyCheck(ts.createElementAccess(entry, 0)), 39 | createValueCheck(ts.createElementAccess(entry, 1)), 40 | ), 41 | 'entry', 42 | ); 43 | 44 | return createLogicalAndChain(createIsInstanceOf(value, ts.createIdentifier('Map')), entryChecks); 45 | }; 46 | 47 | export const createTupleTypeGuard = ( 48 | value: ts.Expression, 49 | length: number, 50 | createElementCheck: (value: ts.Expression, index: number) => ts.Expression, 51 | ): ts.Expression => { 52 | const arrayLengthCheck = ts.createStrictEquality(ts.createPropertyAccess(value, 'length'), ts.createLiteral(length)); 53 | const elementChecks = Array.from({ length }).map((_, index) => 54 | createElementCheck(ts.createElementAccess(value, index), index), 55 | ); 56 | 57 | return createLogicalAndChain(createIsArray(value), arrayLengthCheck, ...elementChecks); 58 | }; 59 | 60 | const createIsNotNumeric = (value: ts.Expression): ts.Expression => 61 | createLogicalAndChain( 62 | ts.createCall(ts.createIdentifier('isNaN'), [], [ts.createCall(ts.createIdentifier('parseFloat'), [], [value])]), 63 | ts.createStrictInequality(value, ts.createLiteral('NaN')), 64 | ); 65 | 66 | export const createObjectTypeGuard = ( 67 | value: ts.Expression, 68 | { properties, numberIndexType, stringIndexType }: ObjectTypeDescriptor, 69 | typeGuardGenerator: TypeGuardGenerator, 70 | ): ts.Expression => { 71 | const objectKeys = createObjectKeys(value); 72 | const basicTypeGuard = 73 | stringIndexType || numberIndexType ? createIsNotPrimitive(value) : createIsNotNullOrUndefined(value); 74 | 75 | const propertyChecks = properties.map(({ type, accessor }) => 76 | typeGuardGenerator(type, ts.createElementAccess(value, accessor)), 77 | ); 78 | 79 | const indexPropertyChecks = 80 | stringIndexType || numberIndexType 81 | ? [ 82 | createArrayEvery( 83 | objectKeys, 84 | (key: ts.Expression) => { 85 | const propertyValue = ts.createElementAccess(value, key); 86 | const numberPropertyCheck = numberIndexType 87 | ? createLogicalOrChain(createIsNotNumeric(key), typeGuardGenerator(numberIndexType, propertyValue)) 88 | : undefined; 89 | 90 | const stringPropertyCheck = stringIndexType 91 | ? typeGuardGenerator(stringIndexType, propertyValue) 92 | : undefined; 93 | 94 | const checks: ts.Expression[] = [numberPropertyCheck, stringPropertyCheck].filter( 95 | Boolean, 96 | ) as ts.Expression[]; 97 | 98 | return createLogicalAndChain(...checks); 99 | }, 100 | 'key', 101 | ), 102 | ] 103 | : []; 104 | 105 | return createLogicalAndChain(basicTypeGuard, ...propertyChecks, ...indexPropertyChecks); 106 | }; 107 | -------------------------------------------------------------------------------- /src/transformer/typeName/debugTypeNameGenerator.ts: -------------------------------------------------------------------------------- 1 | import { TypeName } from '../types'; 2 | import { TypeNameGenerator } from '../types'; 3 | import ts from 'typescript'; 4 | 5 | export const createDebugTypeNameGenerator = (typeChecker: ts.TypeChecker): TypeNameGenerator => { 6 | const existingNames: Set = new Set(); 7 | 8 | return (type: ts.Type): TypeName => { 9 | const originalTypeName = typeChecker.typeToString(type); 10 | let typeName = originalTypeName; 11 | let attempt = 1; 12 | 13 | while (existingNames.has(typeName)) { 14 | typeName = originalTypeName + '~' + ++attempt; 15 | } 16 | 17 | existingNames.add(typeName); 18 | 19 | return typeName; 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /src/transformer/typeName/productionTypeNameGenerator.ts: -------------------------------------------------------------------------------- 1 | import { TypeName } from '../types'; 2 | import { TypeNameGenerator } from '../types'; 3 | 4 | export const createProductionTypeNameGenerator = (): TypeNameGenerator => { 5 | const existingNames: Set = new Set(); 6 | 7 | return (): TypeName => { 8 | let typeName = existingNames.size; 9 | while (existingNames.has(typeName)) typeName++; 10 | 11 | existingNames.add(typeName); 12 | 13 | return String(typeName); 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /src/transformer/typeName/typeNameResolver.ts: -------------------------------------------------------------------------------- 1 | import { TypeDescriptor, TypeName } from '../types'; 2 | import { 3 | TypeDescriptorGenerator, 4 | TypeDescriptorGeneratorCallback, 5 | TypeDescriptorRegistry, 6 | TypeNameResolver, 7 | } from '../types'; 8 | import ts from 'typescript'; 9 | 10 | export const createTypeNameResolver = ( 11 | registry: TypeDescriptorRegistry, 12 | typeDescriptorGenerator: TypeDescriptorGenerator, 13 | ): TypeNameResolver => { 14 | const typeNameResolver = (scope: ts.TypeNode, type: ts.Type): TypeName => { 15 | // FIXME Maybe the registry does not need to pass typeName as the first parameter to create factory 16 | const typeName: TypeName = registry.create(type, () => { 17 | const typeDescriptorOrCallback: TypeDescriptor | TypeDescriptorGeneratorCallback = typeDescriptorGenerator( 18 | scope, 19 | type, 20 | ); 21 | const typeDescriptor: TypeDescriptor = 22 | typeof typeDescriptorOrCallback === 'function' 23 | ? typeDescriptorOrCallback(typeNameResolver) 24 | : typeDescriptorOrCallback; 25 | 26 | return typeDescriptor; 27 | }); 28 | 29 | return typeName; 30 | }; 31 | 32 | return typeNameResolver; 33 | }; 34 | -------------------------------------------------------------------------------- /src/transformer/types.ts: -------------------------------------------------------------------------------- 1 | import ts from 'typescript'; 2 | 3 | /** 4 | * TypeName is defined to make it very explicit when we are talking 5 | * about a type name as opposed to just a string 6 | */ 7 | export type TypeName = string; 8 | 9 | /** 10 | * TypeNameGenerator describes a function that generates (unique) 11 | * string representations for types. 12 | */ 13 | export type TypeNameGenerator = (type: ts.Type) => TypeName; 14 | 15 | /** 16 | * TypeDescriptorGenerator is a function that produces a TypeDescriptor based on a Type 17 | * 18 | * @param scope {ts.TypeNode} The root TypeNode that contains the type 19 | * @param type {ts.Type} The type to resolve into a TypeDescriptor 20 | * 21 | * The scope parameter represented by a TypeNode is necessary to resolve more complex, 22 | * especially generic types. 23 | * 24 | * If the Type to be resolved contains nested types (a union, an intersection etc.) 25 | * it can return a function that will be called with a TypeNameResolver function 26 | * 27 | * @example 28 | * ``` 29 | * const myTypeDescriptorGenerator: TypeDescriptorGenerator = (scope, type) => { 30 | * // ... 31 | * return (resolve: TypeNameResolver) => ({ 32 | * _type: 'union', 33 | * types: type.types.map(unionType => resolve(scope, unionType)) 34 | * }); 35 | * } 36 | * ``` 37 | */ 38 | export type TypeDescriptorGenerator = ( 39 | scope: ts.TypeNode, 40 | type: ts.Type, 41 | ) => TypeDescriptor | TypeDescriptorGeneratorCallback; 42 | 43 | /** 44 | * One of possible return values of TypeDescriptorGenerator 45 | * 46 | * If TypeDescriptorGenerator needs to resolve a type, for example 47 | * an element type of an array or a property type of an interface, 48 | * it will return a TypeDescriptorGeneratorCallback that gives it access 49 | * to a TypeNameResolver. 50 | * 51 | * @param typeNameResolver {TypeNameResolver} 52 | */ 53 | export type TypeDescriptorGeneratorCallback = (typeNameResolver: TypeNameResolver) => TypeDescriptor; 54 | 55 | /** 56 | * TypeNameResolver takes a Type and returns a TypeName that can be used 57 | * to look up a TypeDescriptor from TypeDescriptorRegistry. 58 | * 59 | * @param scope {ts.TypeNode} The root TypeNode that contains the type 60 | * @param type {ts.Type} The type to resolve into a TypeDescriptor 61 | */ 62 | export type TypeNameResolver = (scope: ts.TypeNode, type: ts.Type) => TypeName; 63 | 64 | /** 65 | * TypeGuardGenerator takes a TypeName (that can be used to look up the TypeDescriptor from TypeDescriptorRegistry) 66 | * and turns it into a runtime type guard expression for {@param value}. 67 | * 68 | * @param typeName {TypeName} The name of the type to check against 69 | * @param value {ts.Expression} The value to type check 70 | */ 71 | export type TypeGuardGenerator = (typeName: TypeName, value: ts.Expression) => ts.Expression; 72 | 73 | export type TypeGuardResolver = (typeNode: ts.TypeNode, value: ts.Expression) => ts.Expression; 74 | 75 | export interface TypeDescriptorRegistry { 76 | get(typeName: TypeName): TypeDescriptor | undefined; 77 | create(type: ts.Type, factory: () => TypeDescriptor): TypeName; 78 | } 79 | 80 | /** 81 | * TypeGuardRegistry represents a store for type guard functions 82 | * that might possibly be cyclic. 83 | * 84 | * It stores a type guard function expression that resolves to something like: 85 | * 86 | * @example 87 | * ``` 88 | * (value: unknown): value is MyObject => typeof value === 'object' && ... 89 | * ``` 90 | * 91 | * In order to create the type guard registry in runtime code we need 92 | * to insert the ts.Statement[] returned by code() into the AST. 93 | */ 94 | export interface TypeGuardRegistry { 95 | /** 96 | * Get a type guard function for a type specified by typeName 97 | * 98 | * @param typeName {TypeName} The name of the stored type guard type 99 | * @returns {ts.Expression} Runtime reference to the type guard function (e.g. ___isA___['MyInterface']) 100 | */ 101 | get(typeName: TypeName): ts.Expression | undefined; 102 | 103 | /** 104 | * Create a new type guard function for typeName. 105 | * 106 | * @param typeName {TypeName} The name of the new stored type guard 107 | * @param factory {ExpressionTransformer} Function that accepts a runtime reference to type checked value and returns a type guard function body 108 | * @returns {ts.Expression} Runtime reference to the type guard function (e.g. ___isA___['MyInterface']) 109 | */ 110 | create(typeName: TypeName, factory: ExpressionTransformer): ts.Expression; 111 | 112 | /** 113 | * Export the stored type guards for insertion into the AST 114 | * 115 | * @returns {ts.Statement[]} Statements to be inserted into the AST 116 | */ 117 | code(): ts.Statement[]; 118 | } 119 | 120 | /** 121 | * TypeScript AST visitor that transforms the tree on a per-node basis. 122 | * 123 | * It can either return the original node, return a completely different node 124 | * or return undefined if the original node needs to be removed from the tree. 125 | */ 126 | export type ASTVisitor = (node: ts.Node) => ts.Node | undefined; 127 | 128 | /** 129 | * Helper type for functions that accept an Expression and return a different expression 130 | */ 131 | export type ExpressionTransformer = (value: ts.Expression) => ts.Expression; 132 | 133 | /** 134 | * Helper type for type guard functions 135 | */ 136 | export type TypeGuard = (value: unknown) => value is T; 137 | 138 | /** 139 | * TypeDescriptor is a serializable representation of a Type object. 140 | * It is used as an intermediate step between the type to be checked 141 | * and the generated type guard. 142 | * 143 | * See below for the definitions of all the possible type descriptors 144 | */ 145 | export type TypeDescriptor = 146 | | KeywordTypeDescriptor 147 | | LiteralTypeDescriptor 148 | | FunctionTypeDescriptor 149 | | InterfaceTypeDescriptor 150 | | ArrayTypeDescriptor 151 | | TupleTypeDescriptor 152 | | PromiseTypeDescriptor 153 | | MapTypeDescriptor 154 | | SetTypeDescriptor 155 | | ClassTypeDescriptor 156 | | UnionTypeDescriptor 157 | | IntersectionTypeDescriptor 158 | | NeverTypeDescriptor 159 | | UnspecifiedTypeDescriptor; 160 | 161 | // All the primitive types as specified in https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#object-type 162 | export interface KeywordTypeDescriptor { 163 | _type: 'keyword'; 164 | value: 'string' | 'number' | 'boolean' | 'bigint' | 'object' | 'symbol' | 'null' | 'undefined'; 165 | } 166 | 167 | export interface ObjectTypeDescriptor { 168 | properties: PropertyTypeDescriptor[]; 169 | numberIndexType?: TypeName; 170 | stringIndexType?: TypeName; 171 | } 172 | 173 | export interface FunctionTypeDescriptor extends ObjectTypeDescriptor { 174 | _type: 'function'; 175 | } 176 | 177 | export interface InterfaceTypeDescriptor extends ObjectTypeDescriptor { 178 | _type: 'interface'; 179 | } 180 | 181 | export interface LiteralTypeDescriptor { 182 | _type: 'literal'; 183 | value: ts.Expression; 184 | } 185 | 186 | export interface PropertyTypeDescriptor { 187 | _type: 'property'; 188 | accessor: ts.Expression; 189 | type: TypeName; 190 | } 191 | 192 | export interface ArrayTypeDescriptor { 193 | _type: 'array'; 194 | type: TypeName; 195 | } 196 | 197 | export interface TupleTypeDescriptor { 198 | _type: 'tuple'; 199 | types: TypeName[]; 200 | } 201 | 202 | export interface PromiseTypeDescriptor { 203 | _type: 'promise'; 204 | properties: PropertyTypeDescriptor[]; 205 | } 206 | 207 | export interface MapTypeDescriptor { 208 | _type: 'map'; 209 | keyType: TypeName; 210 | valueType: TypeName; 211 | } 212 | 213 | export interface SetTypeDescriptor { 214 | _type: 'set'; 215 | type: TypeName; 216 | } 217 | 218 | export interface ClassTypeDescriptor { 219 | _type: 'class'; 220 | value: ts.Expression; 221 | } 222 | 223 | export interface UnionTypeDescriptor { 224 | _type: 'union'; 225 | types: TypeName[]; 226 | } 227 | 228 | export interface IntersectionTypeDescriptor { 229 | _type: 'intersection'; 230 | types: TypeName[]; 231 | } 232 | 233 | export interface UnspecifiedTypeDescriptor { 234 | _type: 'unspecified'; 235 | } 236 | 237 | export interface NeverTypeDescriptor { 238 | _type: 'never'; 239 | } 240 | -------------------------------------------------------------------------------- /src/transformer/utils/ast.ts: -------------------------------------------------------------------------------- 1 | import ts from 'typescript'; 2 | 3 | /** 4 | * Helper function that checks whether the array of modifiers 5 | * contains "private" or "protected" keywords. 6 | * 7 | * @param modifiers {ts.ModifiersArray} [undefined] The array of modifiers 8 | */ 9 | const hasPrivateOrProtectedModifiers = (modifiers?: ts.ModifiersArray): boolean => 10 | !!modifiers?.some( 11 | (modifier) => modifier.kind === ts.SyntaxKind.PrivateKeyword || modifier.kind === ts.SyntaxKind.ProtectedKeyword, 12 | ); 13 | 14 | const isPrivateIdentifier = (node: ts.Node): boolean => 15 | typeof ts.isPrivateIdentifier === 'function' ? ts.isPrivateIdentifier(node) : false; 16 | 17 | /** 18 | * Helper function that checks whether a property represented by a Symbol 19 | * is publicly visible, i.e. it does not have "private" or "protected" modifier 20 | * 21 | * @param property {ts.Symbol} Property symbol 22 | */ 23 | export const isPublicProperty = (property: ts.Symbol): boolean => { 24 | const declaration = property.valueDeclaration; 25 | if (!declaration) { 26 | // TODO This is just a "guess", maybe the missing declaration can mean a private/protected property 27 | return true; 28 | } 29 | 30 | if ( 31 | ts.isPropertySignature(declaration) || 32 | ts.isPropertyDeclaration(declaration) || 33 | ts.isMethodDeclaration(declaration) || 34 | ts.isMethodSignature(declaration) || 35 | ts.isParameter(declaration) || 36 | ts.isGetAccessor(declaration) 37 | ) { 38 | if (isPrivateIdentifier(declaration.name)) return false; 39 | 40 | return !hasPrivateOrProtectedModifiers(declaration.modifiers); 41 | } 42 | 43 | return false; 44 | }; 45 | 46 | /** 47 | * Helper function that return property name as a ts.Expression. 48 | * It will make sure that is the property is a numeric literal, 49 | * it is returned as a number rather than a number-like string 50 | * 51 | * @param property {ts.Symbol} The property to get the name of 52 | * @param typeChecker {ts.TypeChecker} Instance of ts.TypeChecker 53 | * @param scope {ts.TypeNode} The root TypeNode that contained the type 54 | */ 55 | const getPropertyName = (property: ts.Symbol, typeChecker: ts.TypeChecker, scope: ts.TypeNode): ts.Expression => { 56 | // Let's get the property type 57 | const propertyType: ts.Type | undefined = 58 | // The nameType property is not documented but can serve as a good starting point, 59 | // saves one function call :) 60 | (property as any).nameType || typeChecker.getTypeOfSymbolAtLocation(property, scope); 61 | 62 | // If the property type exists and it looks like a number literal then let's turn it into a number 63 | if (propertyType && typeof propertyType.flags === 'number' && propertyType.flags & ts.TypeFlags.NumberLiteral) { 64 | const nameAsNumber = parseFloat(property.name); 65 | if (!isNaN(nameAsNumber) && String(nameAsNumber) === property.name) { 66 | return ts.createLiteral(nameAsNumber); 67 | } 68 | } 69 | 70 | return ts.createLiteral(property.name); 71 | }; 72 | 73 | /** 74 | * Helper function that returns a property accessor - either a property name (e.g. 'name') 75 | * or a computed property expression (e.g. Symbol.toStringTag) 76 | * 77 | * @param property {ts.Symbol} The property to get the accessor of 78 | * @param typeChecker {ts.TypeChecker} Instance of ts.TypeChecker 79 | * @param scope {ts.TypeNode} The root TypeNode that contained the type 80 | */ 81 | export const getPropertyAccessor = ( 82 | property: ts.Symbol, 83 | typeChecker: ts.TypeChecker, 84 | scope: ts.TypeNode, 85 | ): ts.Expression => { 86 | const declaration = property.valueDeclaration; 87 | if ( 88 | declaration && 89 | (ts.isPropertySignature(declaration) || 90 | ts.isPropertyDeclaration(declaration) || 91 | ts.isMethodDeclaration(declaration) || 92 | ts.isMethodSignature(declaration)) 93 | ) { 94 | if (ts.isComputedPropertyName(declaration.name)) { 95 | return ts.createIdentifier(declaration.name.expression.getFullText()); 96 | } 97 | } 98 | 99 | return getPropertyName(property, typeChecker, scope); 100 | }; 101 | -------------------------------------------------------------------------------- /src/transformer/utils/codeGenerators.ts: -------------------------------------------------------------------------------- 1 | import ts from 'typescript'; 2 | 3 | export type FunctionBodyCreator = (...args: T) => ts.ConciseBody; 4 | 5 | /** 6 | * Helper function to create an arrow function with one argument 7 | * 8 | * @example 9 | * ``` 10 | * // If we create the function like this 11 | * const arrowFunction = createSingleParameterFunction( 12 | * (argument) => ts.createStrictEquality(argument, ts.createNull()) 13 | * ); 14 | * 15 | * // The generated code will look like this 16 | * (value) => value === null; 17 | * ``` 18 | * 19 | * @example 20 | * ``` 21 | * // Sometimes the argument name would shadow a variable from parent scope 22 | * // In those case we can rename it to something else than 'value' 23 | * const arrowFunction = createSingleParameterFunction( 24 | * (argument) => ts.createStrictEquality(argument, ts.createNull()), 25 | * 'element' 26 | * ); 27 | * 28 | * // The generated code will look like this 29 | * (element) => element === null; 30 | * ``` 31 | * 32 | * @param body {FunctionBodyCreator<[ts.Identifier]>} Function that accepts the argument identifier and returns the function body 33 | * @param argumentName {String} [value] Name of the function argument 34 | */ 35 | export const createSingleParameterFunction = ( 36 | body: FunctionBodyCreator<[ts.Identifier]>, 37 | argumentName = 'value', 38 | ): ts.ArrowFunction => { 39 | const argument: ts.Identifier = ts.createIdentifier(argumentName); 40 | 41 | return ts.createArrowFunction( 42 | /* modifiers */ undefined, 43 | /* typeParameters */ undefined, 44 | [ 45 | ts.createParameter( 46 | /* decorators */ undefined, 47 | /* modifiers */ undefined, 48 | /* dotDotDotToken */ undefined, 49 | /* name */ argument, 50 | ), 51 | ], 52 | undefined, 53 | ts.createToken(ts.SyntaxKind.EqualsGreaterThanToken), 54 | body(argument), 55 | ); 56 | }; 57 | 58 | /** 59 | * Helper function for accessing object properties 60 | * 61 | * @example 62 | * ``` 63 | * const identifier = ts.createIdentifier('obj'); 64 | * 65 | * const accessKey = createElementAccess(identifier, 'property'); 66 | * const accessElement = createElementAccess(identifier, 1); 67 | * 68 | * // The generated code will look like this 69 | * obj['property'] 70 | * obj[1] 71 | * ``` 72 | * 73 | * @param value {ts.Expression} The object which property should be accessed 74 | * @param property {String | Number} The property name / element index 75 | */ 76 | export const createElementAccess = (value: ts.Expression, property: string | number): ts.Expression => 77 | ts.createElementAccess(value, ts.createLiteral(property.toString())); 78 | 79 | export const createObjectWithProperties = (properties: ts.PropertyAssignment[]): ts.Expression => 80 | ts.createObjectLiteral(properties, true); 81 | 82 | export const createVariable = (identifier: ts.Identifier, initializer: ts.Expression): ts.Statement => 83 | ts.createVariableStatement(undefined, [ts.createVariableDeclaration(identifier, undefined, initializer)]); 84 | 85 | export const createArrayEvery = ( 86 | value: ts.Expression, 87 | body: FunctionBodyCreator<[ts.Identifier]>, 88 | callbackArgumentName = 'element', 89 | ): ts.Expression => 90 | ts.createCall( 91 | ts.createPropertyAccess(value, 'every'), 92 | [], 93 | [createSingleParameterFunction(body, callbackArgumentName)], 94 | ); 95 | 96 | export const createArrayFrom = (value: ts.Expression): ts.Expression => 97 | ts.createCall(ts.createPropertyAccess(ts.createIdentifier('Array'), 'from'), [], [value]); 98 | 99 | export const createIsArray = (value: ts.Expression): ts.Expression => 100 | ts.createCall(ts.createPropertyAccess(ts.createIdentifier('Array'), 'isArray'), [], [value]); 101 | 102 | export const createObjectKeys = (value: ts.Expression): ts.Expression => 103 | ts.createCall(ts.createPropertyAccess(ts.createIdentifier('Object'), 'keys'), [], [value]); 104 | 105 | const parenthesize = (expressions: ts.Expression[]): ts.Expression[] => 106 | expressions.map((expression) => 107 | ts.isCallExpression(expression) ? expression : ts.createParen(expression), 108 | ); 109 | 110 | export const createLogicalAndChain = (...expressions: ts.Expression[]): ts.Expression => { 111 | return parenthesize(expressions).reduce((chain, expression) => ts.createLogicalAnd(chain, expression)); 112 | }; 113 | 114 | export const createLogicalOrChain = (...expressions: ts.Expression[]): ts.Expression => { 115 | return parenthesize(expressions).reduce((chain, expression) => ts.createLogicalOr(chain, expression)); 116 | }; 117 | 118 | export const createIsOfType = (value: ts.Expression, type: ts.Expression): ts.Expression => 119 | ts.createStrictEquality(ts.createTypeOf(value), type); 120 | 121 | export const createIsInstanceOf = (value: ts.Expression, className: ts.Expression): ts.Expression => 122 | ts.createBinary(value, ts.SyntaxKind.InstanceOfKeyword, className); 123 | 124 | export const createDoubleNegation = (value: ts.Expression): ts.Expression => 125 | ts.createPrefix(ts.SyntaxKind.ExclamationToken, ts.createPrefix(ts.SyntaxKind.ExclamationToken, value)); 126 | 127 | // See 128 | // 129 | // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#object-type 130 | // 131 | // for more information 132 | export const createIsNotPrimitive = (value: ts.Expression): ts.Expression => { 133 | return createLogicalOrChain( 134 | createIsOfType(value, ts.createLiteral('function')), 135 | createLogicalAndChain(createIsOfType(value, ts.createLiteral('object')), createDoubleNegation(value)), 136 | ); 137 | }; 138 | 139 | export const createIsNotNullOrUndefined = (value: ts.Expression): ts.Expression => 140 | createLogicalAndChain( 141 | ts.createStrictInequality(value, ts.createIdentifier('undefined')), 142 | ts.createStrictInequality(value, ts.createNull()), 143 | ); 144 | 145 | export const createRequire = (identifier: ts.Identifier, path: string, property = 'default'): ts.Statement => 146 | createVariable( 147 | identifier, 148 | ts.createPropertyAccess( 149 | ts.createCall(ts.createIdentifier('require'), undefined, [ts.createLiteral(path)]), 150 | property, 151 | ), 152 | ); 153 | -------------------------------------------------------------------------------- /src/transformer/utils/debug.ts: -------------------------------------------------------------------------------- 1 | import ts from 'typescript'; 2 | 3 | /** 4 | * Helper debugging function that takes a type as a parameter and returns 5 | * a human-readable list of its flags 6 | * 7 | * @param type {ts.Type} 8 | * 9 | * @returns {String[]} Array of type flags names 10 | */ 11 | export const typeFlags = (type: ts.Type): string[] => { 12 | return Object.keys(ts.TypeFlags).filter( 13 | (flagName) => !!((ts.TypeFlags[flagName as ts.TypeFlags] as number) & type.flags), 14 | ); 15 | }; 16 | 17 | /** 18 | * Helper debugging function that takes a type as a parameter and returns 19 | * a human-readable list of its object flags (if it has any) 20 | * 21 | * @param type {ts.Type} 22 | * 23 | * @returns {String[]} Array of object flags names 24 | */ 25 | export const objectFlags = (type: ts.Type): string[] => { 26 | const objectFlags = (type as ts.TypeReference).objectFlags; 27 | if (typeof objectFlags !== 'number') return []; 28 | 29 | return Object.keys(ts.ObjectFlags).filter( 30 | (flagName) => !!((ts.ObjectFlags[flagName as ts.ObjectFlags] as number) & objectFlags), 31 | ); 32 | }; 33 | 34 | /** 35 | * Helper debugging function that takes a Symbol as a parameter and returns 36 | * a human-readable list of its flags 37 | * 38 | * @param type {ts.Symbol} 39 | * 40 | * @returns {String[]} Array of symbol flags names 41 | */ 42 | export const symbolFlags = (symbol: ts.Symbol): string[] => { 43 | return Object.keys(ts.SymbolFlags).filter( 44 | (flagName) => !!((ts.SymbolFlags[flagName as ts.SymbolFlags] as number) & symbol.flags), 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /src/transformer/utils/logger.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | export type LoggerMethod = (...args: unknown[]) => void; 3 | 4 | export interface Logger { 5 | debug: LoggerMethod; 6 | info: LoggerMethod; 7 | warn: LoggerMethod; 8 | error: LoggerMethod; 9 | indent: () => Logger; 10 | } 11 | 12 | export enum LogFeature { 13 | DEBUG = 1, 14 | INFO = 2, 15 | WARN = 4, 16 | ERROR = 8, 17 | } 18 | 19 | export type LogLevel = 'debug' | 'normal' | 'nosey' | 'silent'; 20 | 21 | const LOG_FEATURES: Record = { 22 | debug: LogFeature.DEBUG | LogFeature.INFO | LogFeature.WARN | LogFeature.ERROR, 23 | nosey: LogFeature.INFO | LogFeature.WARN | LogFeature.ERROR, 24 | normal: LogFeature.WARN | LogFeature.ERROR, 25 | silent: 0, 26 | }; 27 | 28 | const noop: () => void = () => undefined; 29 | 30 | const bindConsole = (method: 'debug' | 'info' | 'warn' | 'error', prefix: unknown[]): LoggerMethod => 31 | console[method].bind(console, ...prefix); 32 | 33 | export const createLogger = (logLevel: LogLevel, ...prefix: unknown[]): Logger => { 34 | const logFeatures = LOG_FEATURES[logLevel]; 35 | 36 | return { 37 | debug: logFeatures & LogFeature.DEBUG ? bindConsole('debug', prefix) : noop, 38 | info: logFeatures & LogFeature.INFO ? bindConsole('info', prefix) : noop, 39 | warn: logFeatures & LogFeature.WARN ? bindConsole('warn', prefix) : noop, 40 | error: logFeatures & LogFeature.ERROR ? bindConsole('error', prefix) : noop, 41 | indent: () => createLogger(logLevel, ...prefix, '\t'), 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /src/transformer/utils/transformUsingVisitor.ts: -------------------------------------------------------------------------------- 1 | import { ASTVisitor } from '../types'; 2 | import { visitEachChild } from 'typescript'; 3 | import ts from 'typescript'; 4 | 5 | function visitNode(node: ts.SourceFile, visitor: ASTVisitor): ts.SourceFile; 6 | function visitNode(node: ts.Node, visitor: ASTVisitor): ts.Node | undefined; 7 | function visitNode(node: ts.Node, visitor: ASTVisitor): ts.Node | undefined { 8 | if (ts.isSourceFile(node)) return node; 9 | 10 | return visitor(node); 11 | } 12 | 13 | export function transformUsingVisitor( 14 | node: ts.SourceFile, 15 | context: ts.TransformationContext, 16 | visitor: ASTVisitor, 17 | ): ts.SourceFile; 18 | export function transformUsingVisitor( 19 | node: ts.Node, 20 | context: ts.TransformationContext, 21 | visitor: ASTVisitor, 22 | ): ts.Node | undefined; 23 | export function transformUsingVisitor( 24 | node: ts.Node, 25 | context: ts.TransformationContext, 26 | visitor: ASTVisitor, 27 | ): ts.Node | undefined { 28 | return visitEachChild( 29 | visitNode(node, visitor), 30 | (childNode) => transformUsingVisitor(childNode, context, visitor), 31 | context, 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/transformer/utils/transformerOptions.ts: -------------------------------------------------------------------------------- 1 | import { LogLevel } from './logger'; 2 | 3 | export type TransformerMode = 'development' | 'production'; 4 | 5 | export interface TransformerOptions { 6 | logLevel: LogLevel; 7 | mode: TransformerMode; 8 | } 9 | 10 | const isDevelopment = process.env.NODE_ENVIRONMENT === 'development'; 11 | 12 | export const defaultTransformerOptions: TransformerOptions = { 13 | mode: isDevelopment ? 'development' : 'production', 14 | logLevel: 'normal', 15 | }; 16 | -------------------------------------------------------------------------------- /src/transformer/visitor/assertions.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import path from 'path'; 3 | import ts from 'typescript'; 4 | 5 | const INDEX_JS = path.join(__dirname, 'index.js'); 6 | const INDEX_TS = path.join(__dirname, 'index.d.ts'); 7 | const PWD = execSync('pwd').toString(); 8 | 9 | export const isOurImportExpression = (node: ts.Node): node is ts.ImportDeclaration => { 10 | if (!ts.isImportDeclaration(node)) return false; 11 | 12 | const sourceFile = node.getSourceFile().fileName; 13 | const module = (node.moduleSpecifier as ts.StringLiteral).text; 14 | const isModulePathRelative = module.startsWith('.'); 15 | const modulePath = isModulePathRelative ? path.resolve(path.dirname(sourceFile), module) : module; 16 | 17 | try { 18 | const resolvedPath = require.resolve(modulePath, { paths: [PWD, sourceFile] }); 19 | 20 | return INDEX_JS === resolvedPath; 21 | } catch (e) { 22 | return false; 23 | } 24 | }; 25 | 26 | const isJSDocSignature = (declaration: ts.Node | undefined): declaration is ts.JSDocSignature => { 27 | if (typeof ts.isJSDocSignature !== 'function') return false; 28 | if (!declaration) return false; 29 | 30 | return ts.isJSDocSignature(declaration); 31 | }; 32 | 33 | export const isOurCallExpression = ( 34 | node: ts.Node, 35 | name: string, 36 | typeChecker: ts.TypeChecker, 37 | ): node is ts.CallExpression => { 38 | if (!ts.isCallExpression(node)) return false; 39 | 40 | const declaration = typeChecker.getResolvedSignature(node)?.declaration; 41 | return ( 42 | !!declaration && 43 | // Declaration must be there 44 | !isJSDocSignature(declaration) && 45 | // It has to come from our .d.ts definition file 46 | path.join(declaration.getSourceFile().fileName) === INDEX_TS && 47 | // And its name must match the expected name 48 | declaration.name?.getText() === name 49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /src/transformer/visitor/typeCheckVisitor.ts: -------------------------------------------------------------------------------- 1 | import { ASTVisitor, TypeGuardResolver } from '../types'; 2 | import { createSingleParameterFunction } from '../utils/codeGenerators'; 3 | import { isOurCallExpression, isOurImportExpression } from './assertions'; 4 | import ts from 'typescript'; 5 | 6 | /** 7 | * Factory for ASTVisitor that replaces occurrences of isA and typeCheckFor 8 | * with generated type guards 9 | * 10 | * This visitor inspects the code and uses the typeGuardResolver to generate the type guards 11 | * 12 | * @param typeChecker {ts.TypeChecker} Instance of TypeChecker 13 | * @param typeGuardResolver {TypeCheckFactory} Function that turns a type into a type guard expression 14 | */ 15 | export const createTypeCheckVisitor = ( 16 | typeChecker: ts.TypeChecker, 17 | typeGuardResolver: TypeGuardResolver, 18 | ): ASTVisitor => { 19 | return (node: ts.Node) => { 20 | // All the imports from this module are fake so we need to remove them all 21 | if (isOurImportExpression(node)) return undefined; 22 | 23 | if (isOurCallExpression(node, 'isA', typeChecker)) { 24 | const typeNode = node.typeArguments?.[0]; 25 | if (!typeNode) { 26 | throw new Error('isA() requires one type parameter, none specified'); 27 | } 28 | 29 | const valueNode = node.arguments[0]; 30 | if (!valueNode) { 31 | throw new Error('isA() requires one argument, none specified'); 32 | } 33 | 34 | return typeGuardResolver(typeNode, valueNode); 35 | } 36 | 37 | if (isOurCallExpression(node, 'typeCheckFor', typeChecker)) { 38 | const typeNode = node.typeArguments?.[0]; 39 | if (!typeNode) { 40 | throw new Error('typeCheckFor() requires one type parameter, none specified'); 41 | } 42 | 43 | return createSingleParameterFunction((value) => typeGuardResolver(typeNode, value)); 44 | } 45 | 46 | return node; 47 | }; 48 | }; 49 | -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '../.eslintrc.js', 3 | rules: { 4 | '@typescript-eslint/ban-ts-comment': 0, 5 | '@typescript-eslint/ban-types': 0, 6 | '@typescript-eslint/no-explicit-any': 0, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /test/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | globals: { 4 | 'ts-jest': { 5 | compiler: 'ttypescript', 6 | }, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ts-type-checked/test", 3 | "version": "0.0.1", 4 | "description": "Test project for ts-type-checked", 5 | "private": true, 6 | "scripts": { 7 | "test": "./scripts/test-with-every-version.sh", 8 | "test:version": "./scripts/test-with-version.sh" 9 | }, 10 | "devDependencies": { 11 | "@types/jest": "^26.0.4", 12 | "@types/node": "^14.0.18", 13 | "@types/react": "^16.9.41", 14 | "fast-check": "^1.25.1", 15 | "jest": "^26.1.0", 16 | "react": "^16.13.1", 17 | "semver": "^7.3.2", 18 | "ts-jest": "^26.1.1", 19 | "ts-node": "^8.10.2", 20 | "tslib": "^2.0.0", 21 | "ttypescript": "^1.5.10", 22 | "typescript": "^3.9.6" 23 | }, 24 | "workspaces": [ 25 | "setups/*" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /test/scripts/list-setups-for-typescript.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { exec } = require('child_process'); 4 | const fs = require('fs'); 5 | const semver = require('semver'); 6 | 7 | const argv = process.argv.slice(2); 8 | const typeScriptVersion = argv[0] || ''; 9 | if (!typeScriptVersion) { 10 | console.error('Please specify TypeScript version as the first parameter'); 11 | process.exit(1); 12 | } 13 | 14 | const setupPattern = new RegExp(argv[1] || '', 'ig'); 15 | 16 | exec('yarn workspaces --json info', (error, stdout) => { 17 | if (error) { 18 | throw error; 19 | } 20 | 21 | const yarnOutput = JSON.parse(stdout); 22 | const workspacesByName = JSON.parse(yarnOutput.data); 23 | const workspaceNames = Object.keys(workspacesByName); 24 | 25 | const matchingWorkspaces = workspaceNames.flatMap((workspaceName) => { 26 | if (!setupPattern.test(workspaceName)) { 27 | console.warn(`Excluding setup ${workspaceName}`); 28 | return []; 29 | } 30 | 31 | const workspace = workspacesByName[workspaceName]; 32 | const packageJsonContents = fs.readFileSync(`./${workspace.location}/package.json`, 'utf8'); 33 | const packageJson = JSON.parse(packageJsonContents); 34 | 35 | const workspaceTypeScriptVersion = packageJson.peerDependencies 36 | ? packageJson.peerDependencies.typescript 37 | : undefined; 38 | if (!workspaceTypeScriptVersion) { 39 | throw new Error( 40 | `Test setup ${workspaceName} does not specify compatible typescript versions in peerDependencies!`, 41 | ); 42 | } 43 | 44 | const typeScriptVersionMatchesWorkspace = semver.satisfies(typeScriptVersion, workspaceTypeScriptVersion); 45 | 46 | return typeScriptVersionMatchesWorkspace ? [workspaceName] : []; 47 | }); 48 | 49 | process.stdout.write(matchingWorkspaces.join('\n')); 50 | process.stdout.write('\n'); 51 | }); 52 | -------------------------------------------------------------------------------- /test/scripts/test-with-every-version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script collects all the target TypeScript versions from versions.txt file 4 | # and runs the test suite for each one of them. 5 | 6 | SCRIPTS_PATH=$(dirname $0) 7 | 8 | # Some simple yet cute printing 9 | function printHeader { 10 | echo "########################################" 11 | echo "" 12 | echo "" 13 | echo "$1" 14 | echo "" 15 | echo "" 16 | echo "########################################" 17 | } 18 | 19 | set -e 20 | 21 | # Now for the main act we'll collect all the TS version numbers 22 | # we are interested in and run the test suite for each one, one by one 23 | for VERSION in $(cat $SCRIPTS_PATH/versions.txt); do 24 | # Newlines are allowed and skipped 25 | if [ -z "$VERSION" ]; then 26 | continue 27 | fi 28 | 29 | printHeader "Testing with TypeScript version $VERSION" 30 | 31 | $SCRIPTS_PATH/test-with-version.sh -v "$VERSION" "$@" 32 | 33 | printHeader "Done testing with TypeScript version $VERSION" 34 | done -------------------------------------------------------------------------------- /test/scripts/test-with-version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script is used to run the test suite against a particular version of TypeScript. 4 | # It is important in order to make sure that the transformation is correct for all supported TS versions. 5 | # 6 | # IMPORTANT This script relies on the fact that the code has already been built 7 | # 8 | # The idea is to have a separate package under the `test` folder with its own package.json. 9 | # This package contains the test suite (as well as some test utilities) that will be run. 10 | 11 | # Output coloring 12 | DIMMED='\033[1;30m' 13 | HIGHLIGHT='\033[1;37m' 14 | SUCCESS='\033[0;32m' 15 | ERROR='\033[0;31m' 16 | NC='\033[0m' 17 | 18 | set -e 19 | 20 | DEBUG= 21 | VERSION= 22 | TEST_PATTERN= 23 | SETUP_PATTERN= 24 | 25 | # Get the script arguments 26 | # 27 | # -d|--debug Whether to start jest in node --inspect mode 28 | # -s|--setup The test setup pattern to run (matched against setup package.json name) 29 | # -t|--test The test pattern to run (passed to jest) 30 | # -v|--version The typescript version to test against 31 | while [[ $# -gt 0 ]]; do 32 | OPTION="$1" 33 | 34 | case $OPTION in 35 | -d|--debug) 36 | DEBUG=1 37 | shift # past argument 38 | ;; 39 | -t|--test) 40 | TEST_PATTERN="$2" 41 | shift # past argument 42 | shift # past value 43 | ;; 44 | -s|--setup) 45 | SETUP_PATTERN="$2" 46 | shift # past argument 47 | shift # past value 48 | ;; 49 | -v|--version) 50 | VERSION="$2" 51 | shift # past argument 52 | shift # past value 53 | ;; 54 | *) 55 | shift # past argument 56 | ;; 57 | esac 58 | done 59 | 60 | SCRIPTS_PATH=$(dirname $0) 61 | 62 | if [ -z "$VERSION" ]; then 63 | printf "${ERROR}Please provide a valid typescript version number${NC}\n" 64 | exit 1 65 | fi 66 | 67 | # This function will be executed when the script is terminated 68 | # 69 | # It needs to clean 70 | function cleanup { 71 | # Restore snapshots of package.json and yarn.lock 72 | printf "${DIMMED}Restoring snapshots of package.json and yarn.lock...${NC}\n" 73 | 74 | if [ -f "package.json.snapshot" ]; then 75 | mv package.json.snapshot package.json 76 | fi 77 | 78 | if [ -f "yarn.lock.snapshot" ]; then 79 | mv yarn.lock.snapshot yarn.lock 80 | fi 81 | } 82 | 83 | # Make sure we clean up after the script 84 | trap cleanup EXIT 85 | 86 | printf "${HIGHLIGHT}Running tests for TypeScript version ${VERSION}${NC}\n" 87 | printf "${DIMMED}Using test pattern ${NC}'$TEST_PATTERN'\n" 88 | printf "${DIMMED}Using setup pattern ${NC}'$SETUP_PATTERN'\n" 89 | 90 | # Save snapshots of package.json and yarn.lock 91 | printf "${DIMMED}Saving snapshots of package.json and yarn.lock...${NC}\n" 92 | cp package.json package.json.snapshot 93 | cp yarn.lock yarn.lock.snapshot 94 | 95 | printf "${DIMMED}Installing TypeScript version ${NC}${VERSION}${DIMMED}...${NC}\n" 96 | yarn add --dev --exact --ignore-workspace-root-check typescript@${VERSION} > /dev/null 97 | 98 | # Install dependencies for the whole test monorepo 99 | printf "${DIMMED}Installing dependencies...${NC}\n" 100 | yarn --check-files > /dev/null 101 | 102 | # List all the setups that should be run for this TypeScript version 103 | MATCHING_SETUPS=$($SCRIPTS_PATH/list-setups-for-typescript.js "$VERSION" "$SETUP_PATTERN") 104 | 105 | for MATCHING_SETUP in $MATCHING_SETUPS; do 106 | # Yes the double --silent is necessary :) 107 | MATCHING_TESTS=$(yarn workspace --silent "$MATCHING_SETUP" --silent run jest "$TEST_PATTERN" --listTests) 108 | if [ -z "$MATCHING_TESTS" ]; then 109 | printf "${DIMMED}No matching tests in ${HIGHLIGHT}${MATCHING_SETUP}:${NC}\n" 110 | continue 111 | fi 112 | 113 | printf "${DIMMED}Running setup in ${HIGHLIGHT}${MATCHING_SETUP}:${NC}\n" 114 | 115 | # List all the setups that should be run for this TypeScript version 116 | printf "${DIMMED}TypeScript version verfication: ${NC}" 117 | ACTUAL_VERSION=$(yarn workspace "$MATCHING_SETUP" run tsc --version | grep "Version") 118 | MATCHING_VERSION=$(echo "$ACTUAL_VERSION" | grep "$VERSION") 119 | if [ -z "$MATCHING_VERSION" ]; then 120 | printf "${ERROR}Does not match!${NC}\n" 121 | exit 1 122 | else 123 | printf "${SUCCESS}Matches!${NC}\n" 124 | fi 125 | 126 | printf "${DIMMED}Clearing jest cache...${NC}\n" 127 | yarn workspace "$MATCHING_SETUP" run jest --clearCache 128 | 129 | if [ -z "$DEBUG" ]; then 130 | # In non-debug mode the jest bin is used 131 | yarn workspace "$MATCHING_SETUP" run jest "$TEST_PATTERN" 132 | else 133 | # In debug mode node is started with inspect flag 134 | yarn workspace "$MATCHING_SETUP" node --inspect-brk $(which jest) "$TEST_PATTERN" --runInBand 135 | fi 136 | 137 | printf "\n\n\n" 138 | done 139 | -------------------------------------------------------------------------------- /test/scripts/versions.txt: -------------------------------------------------------------------------------- 1 | 4.0.2 2 | 3.9.2 3 | 3.8.2 4 | 3.7.2 5 | 3.6.2 6 | 3.5.1 7 | 3.4.1 8 | 3.3.1 9 | 3.2.1 10 | 3.1.1 11 | 3.0.1 12 | 2.9.1 13 | 2.8.3 14 | 2.7.2 -------------------------------------------------------------------------------- /test/setups/issue-43--strict-mode/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../../jest.config'); 2 | -------------------------------------------------------------------------------- /test/setups/issue-43--strict-mode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ts-type-checked/test--issue-43--strict-mode", 3 | "version": "0.0.1", 4 | "private": true, 5 | "peerDependencies": { 6 | "typescript": ">=2.8.3" 7 | }, 8 | "dependencies": { 9 | "ts-type-checked": "file:../../../dist" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/setups/issue-43--strict-mode/tests/withStrict.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Primitive, 3 | TypeOf, 4 | assert, 5 | assertArbitrary, 6 | notNullOrUndefined, 7 | notOfType, 8 | nullable, 9 | numeric, 10 | oneOf, 11 | primitive, 12 | symbol, 13 | } from '../../../utils/utils.v2'; 14 | import { isA, typeCheckFor } from 'ts-type-checked'; 15 | import fc from 'fast-check'; 16 | 17 | describe('when strict is true', () => { 18 | describe('primitives', () => { 19 | const nonNullableNotOfType = (type: TypeOf): fc.Arbitrary => 20 | primitive().filter(notOfType(type)).filter(notNullOrUndefined); 21 | 22 | test('number should not be assignable to different primitive types', () => { 23 | assert(numeric(), nonNullableNotOfType('number'), [typeCheckFor(), (value) => isA(value)]); 24 | }); 25 | 26 | test('string should not be assignable to different primitive types', () => { 27 | assert(fc.string(), nonNullableNotOfType('string'), [typeCheckFor(), (value) => isA(value)]); 28 | }); 29 | 30 | test('boolean should not be assignable to different primitive types', () => { 31 | assert(fc.boolean(), nonNullableNotOfType('boolean'), [typeCheckFor(), (value) => isA(value)]); 32 | }); 33 | 34 | test('symbol should not be assignable to different primitive types', () => { 35 | assert(symbol(), nonNullableNotOfType('symbol'), [typeCheckFor(), (value) => isA(value)]); 36 | }); 37 | 38 | test('null and undefined should not be assignable to all primitives', () => { 39 | assertArbitrary(nullable(), [typeCheckFor(), (value) => isA(value)], false); 40 | assertArbitrary(nullable(), [typeCheckFor(), (value) => isA(value)], false); 41 | assertArbitrary(nullable(), [typeCheckFor(), (value) => isA(value)], false); 42 | assertArbitrary(nullable(), [typeCheckFor(), (value) => isA(value)], false); 43 | }); 44 | }); 45 | 46 | type AWSSNSRecordItem = { 47 | EventSource: string; 48 | EventVersion: string; 49 | EventSubscriptionArn: string; 50 | Sns: { 51 | Type?: string; 52 | MessageId?: string; 53 | TopicArn?: string; 54 | Subject?: string; 55 | Message: string; 56 | Timestamp?: string; 57 | SignatureVersion?: string; 58 | Signature?: string; 59 | MessageAttributes?: any; 60 | }; 61 | }; 62 | 63 | type AWSSNSEvent = { 64 | Records: Array; 65 | }; 66 | 67 | const recordItemArbitrary = (): fc.Arbitrary => 68 | fc.record({ 69 | EventSource: fc.string(), 70 | EventVersion: fc.string(), 71 | EventSubscriptionArn: fc.string(), 72 | Sns: fc.record({ 73 | Type: fc.string(), 74 | MessageId: fc.string(), 75 | TopicArn: fc.string(), 76 | Subject: fc.string(), 77 | Message: fc.string(), 78 | Timestamp: fc.string(), 79 | SignatureVersion: fc.string(), 80 | Signature: fc.string(), 81 | MessageAttributes: fc.anything(), 82 | }), 83 | }); 84 | 85 | const eventArbitrary = (): fc.Arbitrary => 86 | fc.record({ 87 | Records: fc.array(recordItemArbitrary()), 88 | }); 89 | 90 | it('null/undefined should not be valid values for any type', () => { 91 | const isEvent = typeCheckFor(); 92 | 93 | const validArbitrary: fc.Arbitrary = oneOf(eventArbitrary()); 94 | const invalidArbitrary = oneOf( 95 | eventArbitrary().map((event) => ({ 96 | ...event, 97 | Records: {}, 98 | })), 99 | fc.constantFrom({ Records: null }, { Records: undefined }, null, undefined), 100 | // Without strict null checks TypeScript is kinda useless - if in this case "Records" 101 | // is null or undefined the check should return true. But that is the case 102 | // for virtually any type - numbers, strings, booleans etc all have undefined "Records" property! 103 | oneOf(fc.string(), numeric(), fc.boolean(), fc.bigInt(), fc.func(fc.anything())) as fc.Arbitrary, 104 | eventArbitrary() 105 | .map((event) => ({ 106 | ...event, 107 | Records: event.Records.map((record) => ({ 108 | ...record, 109 | Sns: { 110 | ...record.Sns, 111 | TopicArn: 7, 112 | }, 113 | })), 114 | })) 115 | .filter((event) => event.Records.length > 0), 116 | ); 117 | 118 | fc.assert( 119 | fc.property(validArbitrary, (event: AWSSNSEvent): void => { 120 | expect(isEvent(event)).toBeTruthy(); 121 | expect(isA(event)).toBeTruthy(); 122 | }), 123 | ); 124 | 125 | fc.assert( 126 | fc.property(invalidArbitrary, (notAnEvent: any): void => { 127 | expect(isEvent(notAnEvent)).toBeFalsy(); 128 | expect(isA(notAnEvent)).toBeFalsy(); 129 | }), 130 | ); 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /test/setups/issue-43--strict-mode/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ES6", 5 | "strict": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/setups/react/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../../jest.config'); 2 | -------------------------------------------------------------------------------- /test/setups/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ts-type-checked/test--react", 3 | "version": "0.0.1", 4 | "private": true, 5 | "peerDependencies": { 6 | "typescript": ">=2.8.3" 7 | }, 8 | "dependencies": { 9 | "ts-type-checked": "file:../../../dist" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/setups/react/tests/react.spec.tsx: -------------------------------------------------------------------------------- 1 | import 'jest'; 2 | import React from 'react'; 3 | 4 | import { assert, notALiteral, notAnObject, notOfType, numeric, oneOf, primitive } from '../../../utils/utils.v2'; 5 | 6 | import { isA, typeCheckFor } from 'ts-type-checked'; 7 | import fc from 'fast-check'; 8 | 9 | describe('React', () => { 10 | const TestFunctionComponent: React.FC = () => null; 11 | class TestClassComponent extends React.Component {} 12 | class TestPureClassComponent extends React.PureComponent {} 13 | 14 | const reactComponentTypeArbitrary: fc.Arbitrary = fc.constantFrom( 15 | TestFunctionComponent, 16 | TestClassComponent, 17 | TestPureClassComponent, 18 | ); 19 | const reactTypeArbitrary: fc.Arbitrary = oneOf( 20 | fc.string(), 21 | reactComponentTypeArbitrary, 22 | ); 23 | const reactKeyArbitrary: fc.Arbitrary = fc.option( 24 | oneOf(fc.string(), numeric()), 25 | ); 26 | const reactPropsArbitrary: fc.Arbitrary = fc.object(); 27 | 28 | describe('ReactElement', () => { 29 | test('without type arguments', () => { 30 | type TypeReference1 = React.ReactElement; 31 | 32 | const validArbitrary = fc.record({ 33 | type: reactTypeArbitrary, 34 | props: reactPropsArbitrary, 35 | key: reactKeyArbitrary, 36 | }); 37 | 38 | const invalidSpecialCases = fc.constantFrom( 39 | {}, 40 | { type: 6, props: {}, key: null }, 41 | { type: {}, props: {}, key: 'key' }, 42 | { props: 'string', key: 'key' }, 43 | { type: 'div', props: {}, key: {} }, 44 | ); 45 | const invalidArbitrary = oneOf( 46 | invalidSpecialCases, 47 | fc.anything().filter(notAnObject), 48 | fc.record({ 49 | props: reactPropsArbitrary, 50 | key: reactKeyArbitrary, 51 | }), 52 | fc.record({ 53 | type: reactPropsArbitrary, 54 | props: reactPropsArbitrary, 55 | key: reactKeyArbitrary, 56 | }), 57 | ); 58 | 59 | assert(validArbitrary, invalidArbitrary, [ 60 | typeCheckFor(), 61 | (value: any) => isA(value), 62 | ]); 63 | }); 64 | 65 | test('with props type argument', () => { 66 | interface TypeReferenceProps { 67 | property: number; 68 | onChange: () => void; 69 | } 70 | type TypeReference1 = React.ReactElement; 71 | 72 | const validPropsArbitrary = fc.record({ 73 | property: numeric(), 74 | onChange: fc.func(fc.constantFrom(undefined)), 75 | }); 76 | const validArbitrary = fc.record({ 77 | type: reactTypeArbitrary, 78 | props: validPropsArbitrary, 79 | key: reactKeyArbitrary, 80 | }); 81 | 82 | const invalidSpecialCases = fc.constantFrom( 83 | { type: 'div', props: {}, key: 'key' }, 84 | { type: 'div', props: { property: 7 }, key: 'key' }, 85 | { type: 'div', props: { property: 7, onChange: undefined }, key: 'key' }, 86 | { type: 'div', props: { property: 'string', onChange: (): void => undefined }, key: 'key' }, 87 | ); 88 | const invalidArbitrary = oneOf( 89 | invalidSpecialCases, 90 | fc.anything().filter(notAnObject), 91 | fc.record({ 92 | type: reactTypeArbitrary, 93 | props: fc.record({ 94 | property: fc.anything().filter(notOfType('number')), 95 | onChange: fc.func(fc.anything()), 96 | }), 97 | key: reactKeyArbitrary, 98 | }), 99 | fc.record({ 100 | type: reactTypeArbitrary, 101 | props: fc.record({ 102 | property: numeric(), 103 | onChange: fc.anything().filter(notOfType('function')), 104 | }), 105 | key: reactKeyArbitrary, 106 | }), 107 | ); 108 | 109 | assert(validArbitrary, invalidArbitrary, [ 110 | typeCheckFor(), 111 | (value: any) => isA(value), 112 | ]); 113 | }); 114 | }); 115 | 116 | test('ComponentType', () => { 117 | type TypeReference1 = React.ComponentType; 118 | 119 | const validArbitrary: fc.Arbitrary = reactComponentTypeArbitrary; 120 | const invalidArbitrary = oneOf( 121 | primitive(), 122 | fc.constantFrom({},
, , , ), 123 | ); 124 | 125 | assert(validArbitrary, invalidArbitrary, [ 126 | typeCheckFor(), 127 | (value: any) => isA(value), 128 | ]); 129 | }); 130 | 131 | test('RefObject', () => { 132 | type TypeReference1 = React.RefObject; 133 | 134 | const validArbitrary = fc.record({ 135 | current: fc.option(fc.string()), 136 | }); 137 | const invalidArbitrary = oneOf( 138 | primitive(), 139 | fc.record({ 140 | current: fc.anything().filter(notOfType('string')).filter(notALiteral(null)), 141 | }), 142 | ); 143 | 144 | assert(validArbitrary, invalidArbitrary, [ 145 | typeCheckFor(), 146 | (value: any) => isA(value), 147 | ]); 148 | }); 149 | }); 150 | -------------------------------------------------------------------------------- /test/setups/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "jsx": "react" 5 | } 6 | } -------------------------------------------------------------------------------- /test/setups/typescript--2.7.2/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../../jest.config'); 2 | -------------------------------------------------------------------------------- /test/setups/typescript--2.7.2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ts-type-checked/test--typescript--2.7.2", 3 | "version": "0.0.1", 4 | "private": true, 5 | "peerDependencies": { 6 | "typescript": ">=2.7.2" 7 | }, 8 | "dependencies": { 9 | "ts-type-checked": "file:../../../dist" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/setups/typescript--2.7.2/tests/arrays.spec.ts: -------------------------------------------------------------------------------- 1 | import 'jest'; 2 | 3 | import { 4 | InterfaceWithPropertyOfType, 5 | assert, 6 | notALiteral, 7 | notAnArray, 8 | notAnEmptyArray, 9 | notOfType, 10 | numeric, 11 | primitive, 12 | } from '../../../utils/utils.v2'; 13 | 14 | import { isA, typeCheckFor } from 'ts-type-checked'; 15 | import fc from 'fast-check'; 16 | 17 | describe('arrays', () => { 18 | test('string[]', () => { 19 | type TypeReference1 = string[]; 20 | 21 | const validArbitrary: fc.Arbitrary = fc.array(fc.string()); 22 | const invalidSpecialCases = fc.constantFrom([6], ['string', true]); 23 | 24 | const invalidArbitrary = fc.oneof( 25 | invalidSpecialCases, 26 | fc.anything().filter(notAnArray), 27 | fc.array(fc.anything().filter(notOfType('string'))).filter(notAnEmptyArray), 28 | ); 29 | 30 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 31 | }); 32 | 33 | test('literal[]', () => { 34 | type LiteralType = 'a' | 'b'; 35 | type TypeReference1 = LiteralType[]; 36 | 37 | const validArbitrary: fc.Arbitrary = fc.array(fc.constantFrom('a', 'b')); 38 | const invalidArbitrary = fc.oneof( 39 | fc.constantFrom([6], ['string', true]), 40 | fc.anything().filter(notAnArray), 41 | fc.array(fc.anything().filter(notOfType('string'))).filter(notAnEmptyArray), 42 | ); 43 | 44 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 45 | }); 46 | 47 | test('interface[]', () => { 48 | type TypeReference1 = InterfaceWithPropertyOfType[]; 49 | 50 | const validArbitrary: fc.Arbitrary = fc.array( 51 | fc.record({ 52 | property: fc.string(), 53 | }), 54 | ); 55 | const invalidArbitrary = fc.oneof( 56 | fc.constantFrom({}, new Object(), [{}], [{ property: 'string' }, false], [[]]), 57 | fc.anything().filter(notAnArray), 58 | fc.array(primitive()).filter(notAnEmptyArray), 59 | fc 60 | .array( 61 | fc.record({ 62 | property: fc.anything().filter(notOfType('string')), 63 | }), 64 | ) 65 | .filter(notAnEmptyArray), 66 | ); 67 | 68 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 69 | }); 70 | 71 | test('tuple', () => { 72 | type TypeReference1 = [number, true, string]; 73 | 74 | const validArbitrary: fc.Arbitrary = fc.tuple(numeric(), fc.constant(true), fc.string()); 75 | const invalidArbitrary = fc.oneof( 76 | fc.anything().filter(notAnArray), 77 | fc.tuple(numeric(), fc.constant(true), fc.string(), fc.anything()), 78 | fc.tuple(fc.anything().filter(notOfType('number')), fc.constant(true), fc.string()), 79 | fc.tuple(numeric(), fc.anything().filter(notALiteral(true)), fc.string()), 80 | fc.tuple(numeric(), fc.constant(true), fc.anything().filter(notOfType('string'))), 81 | ); 82 | 83 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /test/setups/typescript--2.7.2/tests/classes.spec.ts: -------------------------------------------------------------------------------- 1 | import 'jest'; 2 | 3 | import { 4 | assert, 5 | notALiteral, 6 | notNullOrUndefined, 7 | notOfType, 8 | nullable, 9 | numeric, 10 | oneOf, 11 | primitive, 12 | } from '../../../utils/utils.v2'; 13 | import { isA, typeCheckFor } from 'ts-type-checked'; 14 | import fc from 'fast-check'; 15 | 16 | describe('classes', () => { 17 | test('public properties', () => { 18 | class TypeReference1 { 19 | constructor(public property: string) {} 20 | } 21 | 22 | const validArbitrary: fc.Arbitrary = oneOf( 23 | fc.constantFrom( 24 | new TypeReference1('string'), 25 | { property: 'string' }, 26 | Object.assign(() => true, { property: 'string' }), 27 | ), 28 | fc.string().map((value) => new TypeReference1(value)), 29 | fc.record({ 30 | property: fc.string(), 31 | }), 32 | ); 33 | const invalidArbitrary = oneOf( 34 | primitive(), 35 | fc.record({ 36 | property: fc.anything().filter(notOfType('string')), 37 | }), 38 | ); 39 | 40 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 41 | }); 42 | 43 | test('public methods', () => { 44 | class TypeReference1 { 45 | method(): string { 46 | return 'value'; 47 | } 48 | } 49 | 50 | const validArbitrary: fc.Arbitrary = oneOf( 51 | fc.constantFrom( 52 | new TypeReference1(), 53 | { method: () => 'value' }, 54 | Object.assign(() => true, { method: () => 'value' }), 55 | ), 56 | fc.record({ 57 | method: fc.func(fc.anything() as fc.Arbitrary), 58 | }), 59 | ); 60 | const invalidArbitrary = oneOf( 61 | primitive(), 62 | fc.record({ 63 | method: fc.anything().filter(notOfType('function')), 64 | }), 65 | ); 66 | 67 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 68 | }); 69 | 70 | test('public async methods', () => { 71 | class TypeReference1 { 72 | async method() { 73 | return 'value'; 74 | } 75 | } 76 | 77 | const validArbitrary: fc.Arbitrary = oneOf( 78 | fc.constantFrom( 79 | new TypeReference1(), 80 | { method: () => Promise.resolve('value') }, 81 | Object.assign(() => true, { method: () => Promise.resolve('value') }), 82 | ), 83 | fc.record({ 84 | method: fc.func(fc.anything() as fc.Arbitrary), 85 | }), 86 | ); 87 | const invalidArbitrary = oneOf( 88 | primitive(), 89 | fc.record({ 90 | method: fc.anything().filter(notOfType('function')), 91 | }), 92 | ); 93 | 94 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 95 | }); 96 | 97 | test('generic properties', () => { 98 | class TypeReference1 { 99 | constructor(public property: T) {} 100 | } 101 | 102 | const validArbitrary: fc.Arbitrary> = oneOf( 103 | fc.constantFrom( 104 | new TypeReference1(1), 105 | { property: 7 }, 106 | Object.assign(() => true, { property: NaN }), 107 | ), 108 | fc.record({ 109 | property: numeric(), 110 | }), 111 | ); 112 | const invalidArbitrary = oneOf( 113 | primitive(), 114 | fc.record({ 115 | property: fc.anything().filter(notOfType('number')), 116 | }), 117 | ); 118 | 119 | assert(validArbitrary, invalidArbitrary, [ 120 | typeCheckFor>(), 121 | (value) => isA>(value), 122 | ]); 123 | }); 124 | 125 | test('public getters', () => { 126 | class TypeReference1 { 127 | get property1() { 128 | return null; 129 | } 130 | 131 | public get property2(): string { 132 | return ''; 133 | } 134 | } 135 | 136 | const validArbitrary: fc.Arbitrary = oneOf( 137 | fc.constantFrom(new TypeReference1()), 138 | fc.record({ 139 | property1: fc.constant(null), 140 | property2: fc.string(), 141 | }), 142 | ); 143 | 144 | const invalidArbitrary = oneOf( 145 | primitive(), 146 | fc.constantFrom( 147 | {}, 148 | { property1: null }, 149 | { 150 | get property1() { 151 | return 'string'; 152 | }, 153 | property2: 'string', 154 | }, 155 | ), 156 | fc.record({ 157 | property1: fc.anything().filter(notALiteral(null)), 158 | property2: fc.string(), 159 | }), 160 | fc.record({ 161 | property1: fc.constant(null), 162 | property2: fc.anything().filter(notOfType('string')), 163 | }), 164 | ); 165 | 166 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 167 | }); 168 | 169 | test('property initializers', () => { 170 | class TypeReference1 { 171 | property = 'value'; 172 | } 173 | 174 | const validArbitrary: fc.Arbitrary = oneOf( 175 | fc.constantFrom( 176 | new TypeReference1(), 177 | { property: 'string' }, 178 | Object.assign(() => true, { property: 'value' }), 179 | ), 180 | fc.record({ 181 | property: fc.string(), 182 | }), 183 | ); 184 | const invalidArbitrary = oneOf( 185 | primitive(), 186 | fc.record({ 187 | property: fc.anything().filter(notOfType('string')), 188 | }), 189 | ); 190 | 191 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 192 | }); 193 | 194 | test('private properties', () => { 195 | class TypeReference1 { 196 | private property = ''; 197 | private anotherProperty: number; 198 | 199 | constructor(private privateProperty: string, anotherProperty = 1) { 200 | this.anotherProperty = anotherProperty; 201 | } 202 | 203 | private privateMethod(): void {} // eslint-disable-line @typescript-eslint/no-empty-function 204 | private privateMethodWithInitializer = () => {}; // eslint-disable-line @typescript-eslint/no-empty-function 205 | 206 | private get propertyWithGetter() { 207 | return null; 208 | } 209 | } 210 | 211 | const validArbitrary: fc.Arbitrary = oneOf( 212 | fc.constantFrom(new TypeReference1('string'), new TypeReference1('string', 7)), 213 | fc.tuple(fc.string(), numeric()).map(([a, b]) => new TypeReference1(a, b)), 214 | 215 | // Empty object should be valid since TypeReference1 has no public properties 216 | fc.anything().filter(notNullOrUndefined) as fc.Arbitrary, 217 | ); 218 | 219 | const invalidArbitrary = nullable(); 220 | 221 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 222 | }); 223 | 224 | test('protected properties', () => { 225 | class TypeReference1 { 226 | protected property = ''; 227 | protected anotherProperty: number; 228 | 229 | constructor(protected protectedProperty: string, anotherProperty = 1) { 230 | this.anotherProperty = anotherProperty; 231 | } 232 | 233 | protected protectedMethod(): void {} // eslint-disable-line @typescript-eslint/no-empty-function 234 | protected protectedMethodWithInitializer = () => {}; // eslint-disable-line @typescript-eslint/no-empty-function 235 | 236 | protected get propertyWithGetter() { 237 | return null; 238 | } 239 | } 240 | 241 | const validArbitrary: fc.Arbitrary = oneOf( 242 | fc.constantFrom(new TypeReference1('string'), new TypeReference1('string', 7)), 243 | fc.tuple(fc.string(), numeric()).map(([a, b]) => new TypeReference1(a, b)), 244 | 245 | // Empty object should be valid since TypeReference1 has no public properties 246 | fc.anything().filter(notNullOrUndefined) as fc.Arbitrary, 247 | ); 248 | 249 | // Any non-null thing should be valid since it has no properties 250 | const invalidArbitrary = nullable(); 251 | 252 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 253 | }); 254 | }); 255 | -------------------------------------------------------------------------------- /test/setups/typescript--2.7.2/tests/dom.spec.ts: -------------------------------------------------------------------------------- 1 | import 'jest'; 2 | 3 | import { assert } from '../../../utils/utils.v2'; 4 | 5 | import { isA, typeCheckFor } from 'ts-type-checked'; 6 | import fc from 'fast-check'; 7 | 8 | describe('DOM', () => { 9 | test('Document', () => { 10 | type TypeReference1 = Document; 11 | 12 | const validArbitrary = fc.constantFrom(document); 13 | const invalidArbitrary = fc.anything(); 14 | 15 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 16 | }); 17 | 18 | test('Node', () => { 19 | type TypeReference1 = Node; 20 | 21 | const validArbitrary = fc 22 | .constantFrom('div', 'span', 'article', 'p') 23 | .map((tagName) => document.createElement(tagName)); 24 | const invalidArbitrary = fc.anything(); 25 | 26 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 27 | }); 28 | 29 | test('Element', () => { 30 | type TypeReference1 = Element; 31 | 32 | const validArbitrary = fc 33 | .constantFrom('div', 'span', 'article', 'p') 34 | .map((tagName) => document.createElement(tagName)); 35 | const invalidArbitrary = fc.anything(); 36 | 37 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 38 | }); 39 | 40 | test('HTMLDivElement', () => { 41 | type TypeReference1 = HTMLDivElement; 42 | 43 | const validArbitrary = fc.constantFrom('div').map((tagName) => document.createElement(tagName)); 44 | const invalidArbitrary = fc.oneof( 45 | fc.anything(), 46 | fc.constantFrom('span', 'article', 'link', 'p').map((tagName) => document.createElement(tagName)), 47 | ); 48 | 49 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/setups/typescript--2.7.2/tests/enums.spec.ts: -------------------------------------------------------------------------------- 1 | import 'jest'; 2 | 3 | import { assert, notALiteral } from '../../../utils/utils.v2'; 4 | 5 | import { isA, typeCheckFor } from 'ts-type-checked'; 6 | import fc from 'fast-check'; 7 | 8 | describe('enums', () => { 9 | describe('non-const', () => { 10 | test('without values', () => { 11 | enum Enum { 12 | A, 13 | B, 14 | } 15 | 16 | type TypeReference1 = Enum; 17 | 18 | const validArbitrary = fc.constantFrom(Enum.A, Enum.B); 19 | const invalidArbitrary = fc.anything().filter(notALiteral(Enum.A, Enum.B)); 20 | 21 | assert(validArbitrary, invalidArbitrary, [ 22 | typeCheckFor(), 23 | (value: any) => isA(value), 24 | ]); 25 | }); 26 | 27 | test('with values', () => { 28 | enum Enum { 29 | A = 7, 30 | B = 'ole', 31 | } 32 | 33 | type TypeReference1 = Enum; 34 | 35 | const validArbitrary = fc.constantFrom(Enum.A, Enum.B); 36 | const invalidArbitrary = fc.anything().filter(notALiteral(Enum.A, Enum.B)); 37 | 38 | assert(validArbitrary, invalidArbitrary, [ 39 | typeCheckFor(), 40 | (value: any) => isA(value), 41 | ]); 42 | }); 43 | }); 44 | 45 | describe('const', () => { 46 | test('without values', () => { 47 | const enum Enum { 48 | A, 49 | B, 50 | } 51 | 52 | type TypeReference1 = Enum; 53 | 54 | const validArbitrary = fc.constantFrom(Enum.A, Enum.B); 55 | const invalidArbitrary = fc.anything().filter(notALiteral(Enum.A, Enum.B)); 56 | 57 | assert(validArbitrary, invalidArbitrary, [ 58 | typeCheckFor(), 59 | (value: any) => isA(value), 60 | ]); 61 | }); 62 | 63 | test('with values', () => { 64 | const enum Enum { 65 | A = 7, 66 | B = 'ole', 67 | } 68 | 69 | type TypeReference1 = Enum; 70 | 71 | const validArbitrary = fc.constantFrom(Enum.A, Enum.B); 72 | const invalidArbitrary = fc.anything().filter(notALiteral(Enum.A, Enum.B)); 73 | 74 | assert(validArbitrary, invalidArbitrary, [ 75 | typeCheckFor(), 76 | (value: any) => isA(value), 77 | ]); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /test/setups/typescript--2.7.2/tests/indexed.spec.ts: -------------------------------------------------------------------------------- 1 | import 'jest'; 2 | 3 | import { 4 | assert, 5 | notALiteral, 6 | notAnEmptyObject, 7 | notNumeric, 8 | notOfType, 9 | numeric, 10 | oneOf, 11 | primitive, 12 | } from '../../../utils/utils.v2'; 13 | 14 | import { isA, typeCheckFor } from 'ts-type-checked'; 15 | import fc from 'fast-check'; 16 | 17 | describe('string-indexed types', () => { 18 | test('Record', () => { 19 | type TypeReference1 = Record; 20 | 21 | const validArbitrary: fc.Arbitrary = oneOf( 22 | fc.constantFrom( 23 | {}, 24 | new Object() as TypeReference1, 25 | (() => true) as any, 26 | { 6: 7, property: 12 }, 27 | { [Symbol('value-dashed')]: 12 }, 28 | { [Symbol('value')]: 12 }, 29 | { [Symbol('value')]: 'invalid string' }, 30 | Object.assign>(() => true, { age: 6 }), 31 | ), 32 | fc.dictionary(fc.string(), numeric()), 33 | ); 34 | 35 | const invalidArbitrary = oneOf( 36 | fc.constantFrom( 37 | { property: 'string' }, 38 | Object.assign(() => true, { property: 'string' }), 39 | ), 40 | primitive(), 41 | fc.dictionary(fc.string(), fc.anything().filter(notOfType('number'))).filter(notAnEmptyObject), 42 | ); 43 | 44 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 45 | }); 46 | 47 | test('{ [key: string]: number }', () => { 48 | type TypeReference1 = { 49 | [key: string]: number; 50 | }; 51 | 52 | const validArbitrary: fc.Arbitrary = oneOf( 53 | fc.constantFrom( 54 | {}, 55 | { [Symbol('value')]: 'string' } as any, 56 | { [Symbol('value')]: parseInt }, 57 | new Object() as TypeReference1, 58 | { 6: 7, property: 7654e1 }, 59 | Object.assign>(() => true, { 1: 32.123 }), 60 | Object.assign>(() => true, { property: 9 }), 61 | ), 62 | fc.dictionary(fc.string(), numeric()), 63 | fc.dictionary(numeric().map(String), numeric()), 64 | ); 65 | 66 | const invalidArbitrary = oneOf( 67 | fc.constantFrom( 68 | { property: 'string' }, 69 | Object.assign(() => true, { property: 'string' }), 70 | ), 71 | primitive(), 72 | fc.dictionary(fc.string(), fc.anything().filter(notOfType('number'))).filter(notAnEmptyObject), 73 | ); 74 | 75 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 76 | }); 77 | 78 | test('{ [key: number]: number }', () => { 79 | type TypeReference1 = { 80 | [key: number]: number; 81 | }; 82 | 83 | const validArbitrary: fc.Arbitrary = oneOf( 84 | fc.constantFrom( 85 | {}, 86 | { [Symbol('value')]: 'string' } as any, 87 | { [Symbol('value')]: parseInt }, 88 | new Object() as TypeReference1, 89 | { 6: 1, property: () => false }, 90 | { 6: 2344, property: 'string' }, 91 | Object.assign>(() => true, { 6: 1 }), 92 | Object.assign>(() => true, { property: 'string' }), 93 | ), 94 | fc.dictionary(numeric().map(String), numeric()), 95 | fc.dictionary(fc.string().filter(notNumeric), fc.anything()), 96 | ); 97 | 98 | const invalidArbitrary = oneOf( 99 | fc.constantFrom( 100 | { 6: 'string' }, 101 | Object.assign(() => true, { 7: 'string' }), 102 | ), 103 | primitive(), 104 | fc.dictionary(numeric().map(String), fc.anything().filter(notOfType('number'))).filter(notAnEmptyObject), 105 | ); 106 | 107 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 108 | }); 109 | 110 | test('{ [key: number]: "literal", [key: string]: string }', () => { 111 | type TypeReference1 = { 112 | [key: number]: 'literal'; 113 | [key: string]: string; 114 | }; 115 | 116 | const validArbitrary: fc.Arbitrary = oneOf( 117 | fc.constantFrom( 118 | {}, 119 | { [Symbol('value')]: 'string' } as any, 120 | { [Symbol('value')]: parseInt }, 121 | new Object() as TypeReference1, 122 | { 6: 'literal', property: 'string' }, 123 | Object.assign>(() => true, { 6: 'literal' }), 124 | ), 125 | fc.dictionary(numeric().map(String), fc.constant('literal')), 126 | fc.dictionary(fc.string().filter(notNumeric), fc.string()), 127 | ); 128 | 129 | const invalidArbitrary = oneOf( 130 | fc.constantFrom( 131 | { 6: 'string' }, 132 | { 6: 'literal', property: () => false }, 133 | Object.assign(() => true, { 7: 'string' }), 134 | ), 135 | primitive(), 136 | fc.dictionary(numeric().map(String), fc.anything().filter(notALiteral('literal'))).filter(notAnEmptyObject), 137 | ); 138 | 139 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /test/setups/typescript--2.7.2/tests/literals.spec.ts: -------------------------------------------------------------------------------- 1 | import 'jest'; 2 | 3 | import { assert, notALiteral, notAnEmptyArray, notOfType, oneOf, primitive } from '../../../utils/utils.v2'; 4 | 5 | import { isA, typeCheckFor } from 'ts-type-checked'; 6 | import fc from 'fast-check'; 7 | 8 | describe('literals', () => { 9 | describe('singular', () => { 10 | test('string', () => { 11 | type TypeReference1 = 'a'; 12 | 13 | const validArbitrary: fc.Arbitrary = fc.constantFrom('a'); 14 | const invalidArbitrary = fc.anything().filter(notALiteral('a')); 15 | 16 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 17 | }); 18 | 19 | test('number', () => { 20 | type TypeReference1 = 6; 21 | 22 | const validArbitrary: fc.Arbitrary = fc.constantFrom(6); 23 | const invalidArbitrary = fc.anything().filter(notALiteral(6)); 24 | 25 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 26 | }); 27 | 28 | test('true', () => { 29 | type TypeReference1 = true; 30 | 31 | const validArbitrary: fc.Arbitrary = fc.constantFrom(true, !0, !!1); 32 | const invalidArbitrary = fc.anything().filter(notALiteral(true)); 33 | 34 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 35 | }); 36 | 37 | test('false', () => { 38 | type TypeReference1 = false; 39 | 40 | const validArbitrary: fc.Arbitrary = fc.constantFrom(false, !!0, !1); 41 | const invalidArbitrary = fc.anything().filter(notALiteral(false)); 42 | 43 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 44 | }); 45 | }); 46 | 47 | describe('plural', () => { 48 | test('primitive', () => { 49 | type TypeReference1 = 'a' | 6 | false; 50 | 51 | const validArbitrary: fc.Arbitrary = fc.constantFrom('a', 6, false); 52 | const invalidArbitrary = fc.anything().filter(notALiteral('a', 6, false)); 53 | 54 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 55 | }); 56 | 57 | test('non-primitive', () => { 58 | type TypeReference1 = string[] | { property: string }; 59 | 60 | const validArbitrary: fc.Arbitrary = oneOf( 61 | fc.constantFrom( 62 | [], 63 | ['string'], 64 | { property: 'string' }, 65 | Object.assign(() => true, { property: 'string' }), 66 | ), 67 | fc.array(fc.string()), 68 | fc.record({ 69 | property: fc.string(), 70 | }), 71 | ); 72 | const invalidArbitrary = oneOf( 73 | fc.constantFrom([6], ['string', true]), 74 | primitive(), 75 | fc.array(fc.anything().filter(notOfType('string'))).filter(notAnEmptyArray), 76 | fc.record({ 77 | property: fc.anything().filter(notOfType('string')), 78 | }), 79 | ); 80 | 81 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /test/setups/typescript--2.7.2/tests/map.spec.ts: -------------------------------------------------------------------------------- 1 | import 'jest'; 2 | 3 | import { assert, numeric, oneOf, primitive } from '../../../utils/utils.v2'; 4 | 5 | import { isA, typeCheckFor } from 'ts-type-checked'; 6 | import fc from 'fast-check'; 7 | 8 | describe('Map', () => { 9 | test('Map', () => { 10 | type TypeReference1 = Map; 11 | 12 | const validArbitrary: fc.Arbitrary = oneOf( 13 | fc.constantFrom(new Map([[6, 'string']])), 14 | fc.array(fc.tuple(numeric(), fc.string())).map((entries) => new Map(entries)), 15 | ); 16 | const invalidArbitrary = oneOf( 17 | fc.constantFrom({}, new Map([['key', 'value']]), new Set(), new Date()), 18 | primitive(), 19 | ); 20 | 21 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/setups/typescript--2.7.2/tests/promise.spec.ts: -------------------------------------------------------------------------------- 1 | import 'jest'; 2 | 3 | import { assert } from '../../../utils/utils.v2'; 4 | 5 | import { isA, typeCheckFor } from 'ts-type-checked'; 6 | import fc from 'fast-check'; 7 | 8 | describe('Promise', () => { 9 | test('Promise resolution type should not be checked', () => { 10 | type TypeReference1 = Promise; 11 | 12 | const validArbitrary = fc.oneof(fc.anything().map((value) => Promise.resolve(value))); 13 | const invalidArbitrary = fc.anything(); 14 | 15 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/setups/typescript--2.7.2/tests/resolution.spec.ts: -------------------------------------------------------------------------------- 1 | import 'jest'; 2 | 3 | import { 4 | ArrayReference, 5 | GenericReference, 6 | InterfaceWithPropertyOfType, 7 | assert, 8 | notAnArray, 9 | notAnEmptyArray, 10 | notOfType, 11 | numeric, 12 | oneOf, 13 | primitive, 14 | } from '../../../utils/utils.v2'; 15 | 16 | import { isA, typeCheckFor } from 'ts-type-checked'; 17 | import fc from 'fast-check'; 18 | 19 | describe('type resolution', () => { 20 | class A { 21 | aProperty = 'Andrea Bocelli'; 22 | } 23 | 24 | class B { 25 | bProperty = 'Britney Spears'; 26 | } 27 | 28 | const instantiable = fc.constantFrom any>(Object, Array, Number, Boolean, Function, Date, Error, A, B); 29 | 30 | test('simple reference', () => { 31 | type TypeReference1 = string; 32 | type TypeReference2 = TypeReference1; 33 | 34 | const validArbitrary = fc.string(); 35 | const invalidArbitrary = fc.anything().filter(notOfType('string')); 36 | 37 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 38 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 39 | }); 40 | 41 | test('array reference', () => { 42 | type TypeReference1 = string[]; 43 | type TypeReference2 = TypeReference1; 44 | type TypeReference3 = ArrayReference; 45 | 46 | const validArbitrary = fc.array(fc.string()); 47 | const invalidArbitrary = oneOf( 48 | fc.anything().filter(notAnArray), 49 | fc.array(fc.anything().filter(notOfType('string'))).filter(notAnEmptyArray), 50 | ); 51 | 52 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 53 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 54 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 55 | }); 56 | 57 | test('generic reference', () => { 58 | type TypeReference1 = GenericReference; 59 | type TypeReference2 = GenericReference; 60 | type TypeReference3 = GenericReference>; 61 | 62 | const validArbitrary = fc.string(); 63 | const invalidArbitrary = fc.anything().filter(notOfType('string')); 64 | 65 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 66 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 67 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 68 | }); 69 | 70 | test('reference in interface property type', () => { 71 | type TypeReference1 = InterfaceWithPropertyOfType; 72 | type TypeReference2 = { 73 | property: number; 74 | }; 75 | type TypeReference3 = TypeReference1; 76 | interface TypeReference4 { 77 | property: number; 78 | } 79 | 80 | const validArbitrary = fc.record({ 81 | property: numeric(), 82 | }); 83 | const invalidArbitrary = oneOf( 84 | fc.constantFrom({}, { property: 'string' }), 85 | primitive(), 86 | fc.record({ 87 | property: fc.anything().filter(notOfType('number')), 88 | }), 89 | ); 90 | 91 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 92 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 93 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 94 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 95 | }); 96 | 97 | test('signature of a function should not be checked', () => { 98 | type TypeReference1 = (param: boolean) => string; 99 | 100 | const validArbitrary: fc.Arbitrary = fc.func(fc.anything() as fc.Arbitrary); 101 | const invalidArbitrary = oneOf( 102 | fc.constantFrom({}, 'string', false), 103 | fc.anything().filter(notOfType('function')), 104 | ); 105 | 106 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 107 | }); 108 | 109 | test('constructor types', () => { 110 | type TypeReference1 = new () => {}; 111 | 112 | const validArbitrary: fc.Arbitrary = oneOf(instantiable); 113 | const invalidArbitrary = oneOf(fc.anything().filter(notOfType('function'))); 114 | 115 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 116 | }); 117 | 118 | test('interfaces with constructors', () => { 119 | type TypeReference1 = { 120 | new (): {}; 121 | }; 122 | 123 | const validArbitrary: fc.Arbitrary = oneOf(instantiable); 124 | const invalidArbitrary = oneOf(fc.anything().filter(notOfType('function'))); 125 | 126 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 127 | }); 128 | 129 | test('constructor types in unions', () => { 130 | type TypeReference1 = string | (new () => {}); 131 | 132 | const validArbitrary: fc.Arbitrary = oneOf(instantiable, fc.string()); 133 | const invalidArbitrary = oneOf(fc.anything().filter(notOfType('function', 'string'))); 134 | 135 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 136 | }); 137 | }); 138 | -------------------------------------------------------------------------------- /test/setups/typescript--2.7.2/tests/set.spec.ts: -------------------------------------------------------------------------------- 1 | import 'jest'; 2 | 3 | import { assert } from '../../../utils/utils.v2'; 4 | 5 | import { isA, typeCheckFor } from 'ts-type-checked'; 6 | import fc from 'fast-check'; 7 | 8 | describe('Set', () => { 9 | test('Set', () => { 10 | type TypeReference1 = Set; 11 | 12 | const validArbitrary = fc.set(fc.oneof(fc.string(), fc.boolean())).map((values) => new Set(values)); 13 | const invalidArbitrary = fc 14 | .anything() 15 | .filter( 16 | (value) => 17 | !(value instanceof Set) || 18 | Array.from(value.values()).some((element) => typeof element !== 'string' && typeof element !== 'boolean'), 19 | ); 20 | 21 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/setups/typescript--2.7.2/tests/special-cases.spec.ts: -------------------------------------------------------------------------------- 1 | import fc from 'fast-check'; 2 | 3 | import { isA, typeCheckFor } from 'ts-type-checked'; 4 | 5 | describe('special-cases', () => { 6 | const circularTypeError = /^Value that was passed to ts-type-checked contains a circular reference and cannot be checked$/; 7 | 8 | describe('same-type circular structure', () => { 9 | type TypeReference1 = { 10 | next?: TypeReference1; 11 | }; 12 | 13 | const createLinkedList = (n: number, close: boolean): [TypeReference1, TypeReference1] => { 14 | const head: TypeReference1 = {} as TypeReference1; 15 | let tail: TypeReference1 = head; 16 | 17 | // First we create a linear linked list 18 | for (let i = 0; i < n; i++) { 19 | tail = tail.next = {} as TypeReference1; 20 | } 21 | 22 | // Then we close the cycle by pointing the last element to the head 23 | if (close) tail.next = head; 24 | 25 | return [head, tail]; 26 | }; 27 | 28 | test('same-type circular structure should throw an error', () => { 29 | const circularArbitrary = fc.integer(0, 100).map((n) => createLinkedList(n, true)[0]); 30 | 31 | // We check the valid arbitrary on a finer-grained level than the invalid one 32 | fc.assert( 33 | fc.property(circularArbitrary, (value) => { 34 | expect(() => typeCheckFor()(value)).toThrow(circularTypeError); 35 | expect(() => isA(value)).toThrow(circularTypeError); 36 | }) as any, 37 | ); 38 | }); 39 | 40 | // The heap in the cycle breaker might need cleaning so the test should perform one 41 | // check with problematic object, then change its property while not changing the object reference 42 | // and call the check again 43 | test('recovering after same-type circular structure check', () => { 44 | const circularArbitrary = fc 45 | .integer(0, 50) 46 | .map<[TypeReference1, TypeReference1]>((n) => createLinkedList(n, true)); 47 | 48 | // We check the valid arbitrary on a finer-grained level than the invalid one 49 | fc.assert( 50 | fc.property(circularArbitrary, ([head, tail]) => { 51 | expect(() => typeCheckFor()(head)).toThrow(circularTypeError); 52 | expect(() => isA(head)).toThrow(circularTypeError); 53 | 54 | head.next = undefined; 55 | expect(typeCheckFor()(head)).toBeTruthy(); 56 | expect(isA(head)).toBeTruthy(); 57 | 58 | head.next = tail; 59 | expect(() => typeCheckFor()(head)).toThrow(circularTypeError); 60 | expect(() => isA(head)).toThrow(circularTypeError); 61 | }) as any, 62 | ); 63 | }); 64 | }); 65 | 66 | describe('alternating-type circular structure', () => { 67 | type TypeReference1 = { 68 | next?: TypeReference2; 69 | }; 70 | type TypeReference2 = { 71 | following?: TypeReference1; 72 | }; 73 | 74 | const createLinkedList = (n: number, close: boolean): [TypeReference1, TypeReference2] => { 75 | const head: TypeReference1 = {} as TypeReference1; 76 | let tail: TypeReference1 = head; 77 | 78 | // First we create a linear linked list 79 | for (let i = 0; i < n; i++) { 80 | tail.next = {} as TypeReference2; 81 | tail = tail.next.following = {} as TypeReference1; 82 | } 83 | 84 | // Then we close the cycle by pointing the last element to the head 85 | if (close) { 86 | tail.next = {} as TypeReference2; 87 | tail.next.following = head; 88 | } 89 | 90 | return [head, tail.next!]; 91 | }; 92 | 93 | test('alternating-type circular structure should throw an error', () => { 94 | const circularArbitrary = fc.integer(0, 100).map((n) => createLinkedList(n, true)[0]); 95 | 96 | // We check the valid arbitrary on a finer-grained level than the invalid one 97 | fc.assert( 98 | fc.property(circularArbitrary, (value) => { 99 | expect(() => typeCheckFor()(value)).toThrow(circularTypeError); 100 | expect(() => isA(value)).toThrow(circularTypeError); 101 | 102 | expect(() => typeCheckFor()(value.next)).toThrow(circularTypeError); 103 | expect(() => isA(value.next)).toThrow(circularTypeError); 104 | }) as any, 105 | ); 106 | }); 107 | 108 | // The heap in the cycle breaker might need cleaning so the test should perform one 109 | // check with problematic object, then change its property while not changing the object reference 110 | // and call the check again 111 | test('recovering after same-type circular structure check', () => { 112 | const circularArbitrary = fc 113 | .integer(0, 50) 114 | .map<[TypeReference1, TypeReference2]>((n) => createLinkedList(n, true)); 115 | 116 | // We check the valid arbitrary on a finer-grained level than the invalid one 117 | fc.assert( 118 | fc.property(circularArbitrary, ([head, tail]) => { 119 | expect(() => typeCheckFor()(head)).toThrow(circularTypeError); 120 | expect(() => isA(head)).toThrow(circularTypeError); 121 | 122 | tail.following = undefined; 123 | expect(typeCheckFor()(head)).toBeTruthy(); 124 | expect(isA(head)).toBeTruthy(); 125 | 126 | tail.following = head; 127 | expect(() => typeCheckFor()(head)).toThrow(circularTypeError); 128 | expect(() => isA(head)).toThrow(circularTypeError); 129 | }) as any, 130 | ); 131 | }); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /test/setups/typescript--2.7.2/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ES6" 5 | } 6 | } -------------------------------------------------------------------------------- /test/setups/typescript--2.8.3/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../../jest.config'); 2 | -------------------------------------------------------------------------------- /test/setups/typescript--2.8.3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ts-type-checked/test--typescript--2.8.3", 3 | "version": "0.0.1", 4 | "private": true, 5 | "peerDependencies": { 6 | "typescript": ">=2.8.3" 7 | }, 8 | "dependencies": { 9 | "ts-type-checked": "file:../../../dist" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/setups/typescript--2.8.3/tests/conditional.spec.ts: -------------------------------------------------------------------------------- 1 | import 'jest'; 2 | 3 | import { assert, notALiteral, notAnArray, notAnEmptyArray, notOfType } from '../../../utils/utils.v2'; 4 | 5 | // @ts-ignore 6 | import { isA, typeCheckFor } from 'ts-type-checked'; 7 | import fc from 'fast-check'; 8 | 9 | describe('conditional types', () => { 10 | test('conditional types', () => { 11 | type ConditionalOfType = C extends true ? T : undefined; 12 | type PositiveTypeReference = ConditionalOfType; 13 | type NegativeReference = ConditionalOfType; 14 | 15 | const validPositiveArbitrary = fc.array(fc.string()); 16 | const invalidPositiveSpecialCases = fc.constantFrom(['string', 7]); 17 | const invalidPositiveArbitrary = fc.oneof( 18 | invalidPositiveSpecialCases, 19 | fc.anything().filter(notAnArray), 20 | fc.array(fc.anything().filter(notOfType('string'))).filter(notAnEmptyArray), 21 | ); 22 | 23 | const validNegativeArbitrary = fc.constantFrom(undefined, void 0); 24 | const invalidNegativeArbitrary = fc.anything().filter(notOfType('undefined')); 25 | 26 | assert(validPositiveArbitrary, invalidPositiveArbitrary, [ 27 | typeCheckFor(), 28 | (value: any) => isA(value), 29 | ]); 30 | assert(validNegativeArbitrary, invalidNegativeArbitrary, [ 31 | typeCheckFor(), 32 | (value: any) => isA(value), 33 | ]); 34 | }); 35 | 36 | test('never in conditional', () => { 37 | type ConditionalPropertyNames

= { 38 | [K in keyof P]: P[K] extends number ? K : never; 39 | }[keyof P]; 40 | type Interface = { 41 | numeric: number; 42 | string: string; 43 | }; 44 | type TypeReference1 = ConditionalPropertyNames; 45 | const validArbitrary: fc.Arbitrary = fc.oneof(fc.constantFrom('numeric')); 46 | const invalidArbitrary: fc.Arbitrary = fc.oneof( 47 | fc.constantFrom('string'), 48 | fc.anything().filter(notALiteral('numeric')), 49 | ); 50 | assert(validArbitrary, invalidArbitrary, [ 51 | typeCheckFor(), 52 | (value: any) => isA(value), 53 | ]); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/setups/typescript--2.8.3/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } -------------------------------------------------------------------------------- /test/setups/typescript--2.9.1/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../../jest.config'); 2 | -------------------------------------------------------------------------------- /test/setups/typescript--2.9.1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ts-type-checked/test--typescript--2.9.1", 3 | "version": "0.0.1", 4 | "private": true, 5 | "peerDependencies": { 6 | "typescript": ">=2.9.1" 7 | }, 8 | "dependencies": { 9 | "ts-type-checked": "file:../../../dist" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/setups/typescript--2.9.1/tests/indexed.spec.ts: -------------------------------------------------------------------------------- 1 | import 'jest'; 2 | 3 | import { 4 | assert, 5 | notALiteral, 6 | notAnEmptyObject, 7 | notNumeric, 8 | notOfType, 9 | numeric, 10 | primitive, 11 | } from '../../../utils/utils.v2'; 12 | 13 | // @ts-ignore 14 | import { isA, typeCheckFor } from 'ts-type-checked'; 15 | import fc from 'fast-check'; 16 | 17 | describe('number-indexed types', () => { 18 | test('Record', () => { 19 | type TypeReference1 = Record; 20 | 21 | const validArbitrary: fc.Arbitrary = fc.oneof( 22 | fc.constantFrom( 23 | {}, 24 | new Object() as TypeReference1, 25 | (() => true) as any, 26 | { property: 'string' }, 27 | { 6: 7, property: 12 }, 28 | { [Symbol('value')]: 12 }, 29 | { [Symbol('value')]: 'invalid string' }, 30 | Object.assign>(() => true, { 1: 6 }), 31 | ), 32 | fc.dictionary(numeric().map(String), numeric()), 33 | ); 34 | 35 | const invalidArbitrary = fc.oneof( 36 | fc.constantFrom( 37 | { 1: 'string' }, 38 | Object.assign(() => true, { 3: 'string' }), 39 | ), 40 | primitive(), 41 | fc.dictionary(numeric().map(String), fc.anything().filter(notOfType('number'))).filter(notAnEmptyObject), 42 | ); 43 | 44 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 45 | }); 46 | 47 | test('{ [key: number]: number }', () => { 48 | type TypeReference1 = { 49 | [key: number]: number; 50 | }; 51 | 52 | const validArbitrary: fc.Arbitrary = fc.oneof( 53 | fc.constantFrom( 54 | {}, 55 | { [Symbol('value')]: 'string' } as any, 56 | { [Symbol('value')]: parseInt }, 57 | new Object() as TypeReference1, 58 | { 6: 1, property: () => false }, 59 | { 6: 2344, property: 'string' }, 60 | Object.assign>(() => true, { 6: 1 }), 61 | Object.assign>(() => true, { property: 'string' }), 62 | ), 63 | fc.dictionary(numeric().map(String), numeric()), 64 | fc.dictionary(fc.string().filter(notNumeric), fc.anything()), 65 | ); 66 | 67 | const invalidArbitrary = fc.oneof( 68 | fc.constantFrom( 69 | { 6: 'string' }, 70 | Object.assign(() => true, { 7: 'string' }), 71 | ), 72 | primitive(), 73 | fc.dictionary(numeric().map(String), fc.anything().filter(notOfType('number'))).filter(notAnEmptyObject), 74 | ); 75 | 76 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 77 | }); 78 | 79 | test('{ [key: number]: "literal", [key: string]: string }', () => { 80 | type TypeReference1 = { 81 | [key: number]: 'literal'; 82 | [key: string]: string; 83 | }; 84 | 85 | const validArbitrary: fc.Arbitrary = fc.oneof( 86 | fc.constantFrom( 87 | {}, 88 | { [Symbol('value')]: 'string' } as any, 89 | { [Symbol('value')]: parseInt }, 90 | new Object() as TypeReference1, 91 | { 6: 'literal', property: 'string' }, 92 | Object.assign>(() => true, { 6: 'literal' }), 93 | ), 94 | fc.dictionary(numeric().map(String), fc.constant('literal')), 95 | fc.dictionary(fc.string().filter(notNumeric), fc.string()), 96 | ); 97 | 98 | const invalidArbitrary = fc.oneof( 99 | fc.constantFrom( 100 | { 6: 'string' }, 101 | { 6: 'literal', property: () => false }, 102 | Object.assign(() => true, { 7: 'string' }), 103 | ), 104 | primitive(), 105 | fc.dictionary(numeric().map(String), fc.anything().filter(notALiteral('literal'))).filter(notAnEmptyObject), 106 | ); 107 | 108 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /test/setups/typescript--2.9.1/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } -------------------------------------------------------------------------------- /test/setups/typescript--3.0.1/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../../jest.config'); 2 | -------------------------------------------------------------------------------- /test/setups/typescript--3.0.1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ts-type-checked/test--typescript--3.0.1", 3 | "version": "0.0.1", 4 | "private": true, 5 | "peerDependencies": { 6 | "typescript": ">=3.0.1" 7 | }, 8 | "dependencies": { 9 | "ts-type-checked": "file:../../../dist" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/setups/typescript--3.0.1/tests/unknown.spec.ts: -------------------------------------------------------------------------------- 1 | import 'jest'; 2 | 3 | import { assertArbitrary } from '../../../utils/utils.v2'; 4 | 5 | // @ts-ignore 6 | import { isA, typeCheckFor } from 'ts-type-checked'; 7 | import fc from 'fast-check'; 8 | 9 | describe('unknown', () => { 10 | test('unknown', () => { 11 | type TypeReference1 = unknown; 12 | 13 | const validArbitrary: fc.Arbitrary = fc.anything(); 14 | 15 | assertArbitrary( 16 | validArbitrary, 17 | [typeCheckFor(), (value: unknown) => isA(value)], 18 | true, 19 | ); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/setups/typescript--3.0.1/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } -------------------------------------------------------------------------------- /test/setups/typescript--3.2.1/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../../jest.config'); 2 | -------------------------------------------------------------------------------- /test/setups/typescript--3.2.1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ts-type-checked/test--typescript--3.2.1", 3 | "version": "0.0.1", 4 | "private": true, 5 | "peerDependencies": { 6 | "typescript": ">=3.2.1" 7 | }, 8 | "dependencies": { 9 | "ts-type-checked": "file:../../../dist" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/setups/typescript--3.2.1/tests/bigint.spec.ts: -------------------------------------------------------------------------------- 1 | import 'jest'; 2 | 3 | import { assert, notOfType } from '../../../utils/utils.v3'; 4 | 5 | import { isA, typeCheckFor } from 'ts-type-checked'; 6 | import { notALiteral, nullable } from '../../../utils/utils.v2'; 7 | import fc from 'fast-check'; 8 | 9 | describe('bigint', () => { 10 | test('bigint', () => { 11 | type TypeReference1 = bigint; 12 | 13 | const validArbitrary: fc.Arbitrary = fc.bigInt(); 14 | const invalidArbitrary = fc.anything().filter(notOfType('bigint')); 15 | 16 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 17 | }); 18 | 19 | test('BigInt', () => { 20 | type TypeReference1 = BigInt; // eslint-disable-line @typescript-eslint/ban-types 21 | 22 | const validArbitrary: fc.Arbitrary = fc.bigInt(); 23 | const invalidArbitrary = fc.anything().filter(notOfType('bigint')); 24 | 25 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 26 | }); 27 | 28 | test('bigint literal', () => { 29 | type TypeReference1 = 1n; 30 | 31 | const validArbitrary: fc.Arbitrary = fc.constantFrom(1n); 32 | const invalidArbitrary = fc.anything().filter(notALiteral(1n)); 33 | 34 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 35 | }); 36 | 37 | test('bigint literal in union', () => { 38 | type TypeReference1 = 'a' | 6 | false | 7n; 39 | 40 | const validArbitrary: fc.Arbitrary = fc.constantFrom('a', 6, false, 7n); 41 | const invalidArbitrary = fc.anything().filter(notALiteral('a', 6, false, 7n)); 42 | 43 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 44 | }); 45 | 46 | test('{}', () => { 47 | type TypeReference1 = {}; 48 | 49 | const validArbitrary: fc.Arbitrary = fc.oneof(fc.constantFrom(1n), fc.bigInt()); 50 | const invalidArbitrary = nullable(); 51 | 52 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 53 | }); 54 | 55 | test('with Object methods', () => { 56 | interface TypeReference1 { 57 | toString: () => string; 58 | } 59 | 60 | const validArbitrary: fc.Arbitrary = fc.oneof(fc.constantFrom(6, 6n), fc.bigInt()); 61 | 62 | const invalidArbitrary = fc.oneof(nullable()); 63 | 64 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /test/setups/typescript--3.2.1/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "target": "ESNext" 6 | } 7 | } -------------------------------------------------------------------------------- /test/setups/typescript--3.8.2/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../../jest.config'); 2 | -------------------------------------------------------------------------------- /test/setups/typescript--3.8.2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ts-type-checked/test--typescript--3.8.2", 3 | "version": "0.0.1", 4 | "private": true, 5 | "peerDependencies": { 6 | "typescript": ">=3.8.2" 7 | }, 8 | "dependencies": { 9 | "ts-type-checked": "file:../../../dist" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/setups/typescript--3.8.2/tests/classes.spec.ts: -------------------------------------------------------------------------------- 1 | import 'jest'; 2 | 3 | import { assert, notNullOrUndefined, nullable, numeric, oneOf } from '../../../utils/utils.v2'; 4 | import { isA, typeCheckFor } from 'ts-type-checked'; 5 | import fc from 'fast-check'; 6 | 7 | describe('classes', () => { 8 | test('ES6 private properties', () => { 9 | class TypeReference1 { 10 | #property = ''; 11 | #anotherProperty: number; 12 | 13 | constructor(anotherProperty = 1) { 14 | this.#anotherProperty = anotherProperty; 15 | } 16 | 17 | #privateMethodWithInitializer = () => {}; // eslint-disable-line @typescript-eslint/no-empty-function 18 | } 19 | 20 | const validArbitrary: fc.Arbitrary = oneOf( 21 | fc.constantFrom(new TypeReference1(6), new TypeReference1()), 22 | numeric().map((a) => new TypeReference1(a)), 23 | 24 | // Empty object should be valid since TypeReference1 has no public properties 25 | fc.anything().filter(notNullOrUndefined) as fc.Arbitrary, 26 | ); 27 | 28 | const invalidArbitrary = nullable(); 29 | 30 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/setups/typescript--3.8.2/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "target": "ESNext" 6 | } 7 | } -------------------------------------------------------------------------------- /test/setups/typescript--4.0.2/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../../jest.config'); 2 | -------------------------------------------------------------------------------- /test/setups/typescript--4.0.2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ts-type-checked/test--typescript--4.0.2", 3 | "version": "0.0.1", 4 | "private": true, 5 | "peerDependencies": { 6 | "typescript": ">=4.0.2" 7 | }, 8 | "dependencies": { 9 | "ts-type-checked": "file:../../../dist" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/setups/typescript--4.0.2/tests/literals.spec.ts: -------------------------------------------------------------------------------- 1 | import 'jest'; 2 | 3 | import { assert, oneOf } from '../../../utils/utils.v2'; 4 | import { isA, typeCheckFor } from 'ts-type-checked'; 5 | import fc from 'fast-check'; 6 | 7 | describe('literals', () => { 8 | test('false', () => { 9 | const validArbitrary: fc.Arbitrary = oneOf(fc.constantFrom(false)); 10 | const invalidArbitrary = fc.anything().filter((value) => value !== false); 11 | 12 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 13 | }); 14 | 15 | test('true', () => { 16 | const validArbitrary: fc.Arbitrary = oneOf(fc.constantFrom(true)); 17 | const invalidArbitrary = fc.anything().filter((value) => value !== true); 18 | 19 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 20 | }); 21 | 22 | test('false (aliased)', () => { 23 | type TypeReference1 = false; 24 | 25 | const validArbitrary: fc.Arbitrary = oneOf(fc.constantFrom(false)); 26 | const invalidArbitrary = fc.anything().filter((value) => value !== false); 27 | 28 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 29 | }); 30 | 31 | test('true (aliased)', () => { 32 | type TypeReference1 = true; 33 | 34 | const validArbitrary: fc.Arbitrary = oneOf(fc.constantFrom(true)); 35 | const invalidArbitrary = fc.anything().filter((value) => value !== true); 36 | 37 | assert(validArbitrary, invalidArbitrary, [typeCheckFor(), (value) => isA(value)]); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/setups/typescript--4.0.2/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "target": "ESNext" 6 | } 7 | } -------------------------------------------------------------------------------- /test/setups/without-strict-mode/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../../jest.config'); 2 | -------------------------------------------------------------------------------- /test/setups/without-strict-mode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ts-type-checked/test--without-strict-mode", 3 | "version": "0.0.1", 4 | "private": true, 5 | "peerDependencies": { 6 | "typescript": ">=2.7.2" 7 | }, 8 | "dependencies": { 9 | "ts-type-checked": "file:../../../dist" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/setups/without-strict-mode/tests/withoutStrictMode.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Primitive, 3 | TypeOf, 4 | assert, 5 | assertArbitrary, 6 | notNullOrUndefined, 7 | notOfType, 8 | nullable, 9 | numeric, 10 | oneOf, 11 | primitive, 12 | symbol, 13 | } from '../../../utils/utils.v2'; 14 | import { isA, typeCheckFor } from 'ts-type-checked'; 15 | import fc from 'fast-check'; 16 | 17 | describe('without strict mode', () => { 18 | describe('primitives', () => { 19 | const nonNullableNotOfType = (type: TypeOf): fc.Arbitrary => 20 | primitive().filter(notOfType(type)).filter(notNullOrUndefined); 21 | 22 | test('number should not be assignable to different primitive types', () => { 23 | assert(numeric(), nonNullableNotOfType('number'), [typeCheckFor(), (value) => isA(value)]); 24 | }); 25 | 26 | test('string should not be assignable to different primitive types', () => { 27 | assert(fc.string(), nonNullableNotOfType('string'), [typeCheckFor(), (value) => isA(value)]); 28 | }); 29 | 30 | test('boolean should not be assignable to different primitive types', () => { 31 | assert(fc.boolean(), nonNullableNotOfType('boolean'), [typeCheckFor(), (value) => isA(value)]); 32 | }); 33 | 34 | test('symbol should not be assignable to different primitive types', () => { 35 | assert(symbol(), nonNullableNotOfType('symbol'), [typeCheckFor(), (value) => isA(value)]); 36 | }); 37 | 38 | test('null and undefined should be assignable to all primitives', () => { 39 | assertArbitrary(nullable(), [typeCheckFor(), (value) => isA(value)], true); 40 | assertArbitrary(nullable(), [typeCheckFor(), (value) => isA(value)], true); 41 | assertArbitrary(nullable(), [typeCheckFor(), (value) => isA(value)], true); 42 | assertArbitrary(nullable(), [typeCheckFor(), (value) => isA(value)], true); 43 | }); 44 | }); 45 | 46 | type AWSSNSRecordItem = { 47 | EventSource: string; 48 | EventVersion: string; 49 | EventSubscriptionArn: string; 50 | Sns: { 51 | Type?: string; 52 | MessageId?: string; 53 | TopicArn?: string; 54 | Subject?: string; 55 | Message: string; 56 | Timestamp?: string; 57 | SignatureVersion?: string; 58 | Signature?: string; 59 | MessageAttributes?: any; 60 | }; 61 | }; 62 | 63 | type AWSSNSEvent = { 64 | Records: Array; 65 | }; 66 | 67 | const optionalOf = (arbitrary: fc.Arbitrary): fc.Arbitrary => 68 | oneOf(nullable(), arbitrary); 69 | 70 | const recordItemArbitrary = (): fc.Arbitrary => 71 | fc.record({ 72 | EventSource: optionalOf(fc.string()), 73 | EventVersion: optionalOf(fc.string()), 74 | EventSubscriptionArn: optionalOf(fc.string()), 75 | Sns: optionalOf( 76 | fc.record({ 77 | Type: optionalOf(fc.string()), 78 | MessageId: optionalOf(fc.string()), 79 | TopicArn: optionalOf(fc.string()), 80 | Subject: optionalOf(fc.string()), 81 | Message: optionalOf(fc.string()), 82 | Timestamp: optionalOf(fc.string()), 83 | SignatureVersion: optionalOf(fc.string()), 84 | Signature: optionalOf(fc.string()), 85 | MessageAttributes: fc.anything(), 86 | }), 87 | ), 88 | }); 89 | 90 | const eventArbitrary = (): fc.Arbitrary => 91 | fc.record({ 92 | Records: fc.array(recordItemArbitrary()), 93 | }); 94 | 95 | it('null/undefined should be valid values for any type', () => { 96 | const isEvent = typeCheckFor(); 97 | 98 | const validArbitrary: fc.Arbitrary = oneOf( 99 | eventArbitrary(), 100 | fc.constantFrom({ Records: null }, { Records: undefined }, null, undefined), 101 | // Without strict null checks TypeScript is kinda useless - if in this case "Records" 102 | // is null or undefined the check should return true. But that is the case 103 | // for virtually any type - numbers, strings, booleans etc all have undefined "Records" property! 104 | oneOf(fc.string(), numeric(), fc.boolean(), fc.bigInt(), fc.func(fc.anything())) as fc.Arbitrary, 105 | ); 106 | const invalidArbitrary = oneOf( 107 | eventArbitrary().map((event) => ({ 108 | ...event, 109 | Records: {}, 110 | })), 111 | eventArbitrary() 112 | .map((event) => ({ 113 | ...event, 114 | Records: event.Records.map((record) => ({ 115 | ...record, 116 | Sns: { 117 | ...record.Sns, 118 | TopicArn: 7, 119 | }, 120 | })), 121 | })) 122 | .filter((event) => event.Records.length > 0), 123 | ); 124 | 125 | fc.assert( 126 | fc.property(validArbitrary, (event: AWSSNSEvent): void => { 127 | expect(isEvent(event)).toBeTruthy(); 128 | expect(isA(event)).toBeTruthy(); 129 | }) as any, 130 | ); 131 | 132 | fc.assert( 133 | fc.property(invalidArbitrary, (notAnEvent: any): void => { 134 | expect(isEvent(notAnEvent)).toBeFalsy(); 135 | expect(isA(notAnEvent)).toBeFalsy(); 136 | }) as any, 137 | ); 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /test/setups/without-strict-mode/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ES6", 5 | "strictNullChecks": false, 6 | "strictPropertyInitialization": false 7 | } 8 | } -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "plugins": [ 5 | { "transform": "ts-type-checked/transformer", "logLevel": "normal", "mode": "development" }, 6 | ] 7 | }, 8 | "include": ["setups", "utils"], 9 | "exclude": ["node_modules"] 10 | } -------------------------------------------------------------------------------- /test/utils/utils.v2.ts: -------------------------------------------------------------------------------- 1 | import 'jest'; 2 | import fc from 'fast-check'; 3 | 4 | export type GenericReference = T; 5 | 6 | export type ArrayReference = T[]; 7 | 8 | export interface InterfaceWithPropertyOfType { 9 | property: T; 10 | } 11 | 12 | export interface InterfaceWithPropertiesOfTypes { 13 | property1: T; 14 | property2: U; 15 | } 16 | 17 | export interface InterfaceWithDifferentPropertyOfType { 18 | differentProperty: T; 19 | } 20 | 21 | export const oneOf = (...arbitraries: fc.Arbitrary[]): fc.Arbitrary => 22 | // TypeScript 2.7.2 does not work well with fast-check type definitions 23 | fc.oneof(...arbitraries) as fc.Arbitrary; 24 | 25 | export const optionalOf = (arbitrary: fc.Arbitrary): fc.Arbitrary => 26 | oneOf(arbitrary, fc.constant(undefined)); 27 | 28 | export const symbol = (): fc.Arbitrary => fc.string().map(Symbol); 29 | 30 | export const nullable = (): fc.Arbitrary => fc.constantFrom(null, undefined); 31 | 32 | export const numeric = (): fc.Arbitrary => 33 | oneOf(fc.integer(), fc.float(Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY)); 34 | 35 | export const primitive = (): fc.Arbitrary => 36 | oneOf(fc.string(), symbol(), fc.boolean(), numeric(), nullable()) as fc.Arbitrary; 37 | 38 | // Filtering functions for arbitraries 39 | export type FilterFunction = (value: any) => boolean; 40 | export type TypeOf = 'string' | 'boolean' | 'number' | 'function' | 'undefined' | 'object' | 'symbol'; 41 | export type Primitive = string | boolean | number | symbol | null | undefined; 42 | 43 | export const notAnArray: FilterFunction = (value: any): boolean => !Array.isArray(value); 44 | export const notAnEmptyArray: FilterFunction = (value: any): boolean => !Array.isArray(value) || value.length !== 0; 45 | export const notAnObject: FilterFunction = (value: any): boolean => typeof value !== 'object' || value === null; 46 | export const notAnEmptyObject: FilterFunction = (value: any): boolean => Object.keys(value as any).length !== 0; 47 | export const notOfType = (...types: TypeOf[]): FilterFunction => (value: any): boolean => 48 | types.indexOf(typeof value as TypeOf) === -1; 49 | export const notALiteral = (...literals: any[]): FilterFunction => (value: any): boolean => 50 | literals.indexOf(value) === -1; 51 | export const notNumeric: FilterFunction = (value) => isNaN(parseFloat(value as any)); 52 | export const notNullOrUndefined: FilterFunction = (value) => value !== null && value !== undefined; 53 | export const notA = (constructor: Function): FilterFunction => (value) => !(value instanceof constructor); 54 | 55 | // Helper assertion methods 56 | export const assertArbitrary = (arbitrary: fc.Arbitrary, checks: FilterFunction[], result: boolean): void => { 57 | fc.assert( 58 | fc.property(arbitrary, (value) => { 59 | checks.forEach((check) => { 60 | expect(check(value)).toBe(result); 61 | }); 62 | // TypeScript 2.7.2 does not work well with fast-check type definitions 63 | }) as any, 64 | ); 65 | }; 66 | 67 | export const assert = ( 68 | validArbitrary: fc.Arbitrary, 69 | invalidArbitrary: fc.Arbitrary, 70 | checks: FilterFunction[], 71 | ): void => { 72 | assertArbitrary(validArbitrary, checks, true); 73 | assertArbitrary(invalidArbitrary, checks, false); 74 | }; 75 | -------------------------------------------------------------------------------- /test/utils/utils.v3.ts: -------------------------------------------------------------------------------- 1 | import 'jest'; 2 | import { numeric } from './utils.v2'; 3 | import fc from 'fast-check'; 4 | 5 | export const primitive = (): fc.Arbitrary => 6 | fc.oneof(fc.string(), fc.boolean(), numeric(), fc.bigInt(), fc.constantFrom(null, undefined, Symbol('a'))); 7 | 8 | // Filtering functions for arbitraries 9 | export type FilterFunction = (value: unknown) => boolean; 10 | type TypeOf = 'string' | 'boolean' | 'number' | 'function' | 'bigint' | 'undefined' | 'object' | 'symbol'; 11 | type Primitive = string | boolean | number | bigint | symbol | null | undefined; 12 | 13 | export const notOfType = (...types: TypeOf[]): FilterFunction => (value: unknown): boolean => 14 | !types.includes(typeof value); 15 | 16 | // Helper assertion methods 17 | export const assertArbitrary = (arbitrary: fc.Arbitrary, checks: FilterFunction[], result: boolean): void => { 18 | fc.assert( 19 | fc.property(arbitrary, (value) => { 20 | checks.forEach((check) => { 21 | expect(check(value)).toBe(result); 22 | }); 23 | }), 24 | ); 25 | }; 26 | 27 | export const assert = ( 28 | validArbitrary: fc.Arbitrary, 29 | invalidArbitrary: fc.Arbitrary, 30 | checks: FilterFunction[], 31 | ): void => { 32 | assertArbitrary(validArbitrary, checks, true); 33 | assertArbitrary(invalidArbitrary, checks, false); 34 | }; 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "emitDecoratorMetadata": true, 5 | "esModuleInterop": true, 6 | "experimentalDecorators": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "importHelpers": true, 9 | "declaration": true, 10 | "noEmit": false, 11 | "noErrorTruncation": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "noImplicitAny": true, 14 | "noImplicitThis": false, 15 | "noImplicitReturns": true, 16 | "noUnusedLocals": false, 17 | "noUnusedParameters": false, 18 | "preserveConstEnums": true, 19 | "removeComments": false, 20 | "skipLibCheck": true, 21 | "sourceMap": false, 22 | "strict": true, 23 | "strictFunctionTypes": true, 24 | "strictNullChecks": true, 25 | "strictPropertyInitialization": true, 26 | "suppressImplicitAnyIndexErrors": false, 27 | "moduleResolution": "node", 28 | "target": "es6", 29 | "module": "commonjs", 30 | "lib": [ 31 | "dom", 32 | "es6" 33 | ] 34 | }, 35 | "exclude": ["node_modules", "dist"] 36 | } --------------------------------------------------------------------------------