├── .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 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
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