├── .eslintrc.js ├── .gitattributes ├── .github └── workflows │ ├── main.yml │ └── size.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .yarn └── releases │ └── yarn-4.5.3.cjs ├── .yarnrc.yml ├── LICENSE ├── README.md ├── package.json ├── src ├── getParsedStack.ts ├── helperTypes.ts └── index.ts ├── test └── index.test.ts ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-undef 2 | module.exports = { 3 | env: { es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/eslint-recommended', 7 | 'plugin:@typescript-eslint/recommended', 8 | 'plugin:typescript-sort-keys/recommended', 9 | 'prettier', 10 | 'plugin:prettier/recommended', // should always be at the end 11 | ], 12 | ignorePatterns: ['dist/**/*'], 13 | parser: '@typescript-eslint/parser', 14 | parserOptions: { ecmaVersion: 2020 }, 15 | plugins: [ 16 | '@typescript-eslint', 17 | 'prettier', 18 | 'typescript-sort-keys', 19 | 'simple-import-sort', 20 | 'import', 21 | ], 22 | rules: { 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | '@typescript-eslint/no-unused-vars': [ 25 | 'error', 26 | { 27 | args: 'after-used', 28 | argsIgnorePattern: '^_', 29 | caughtErrors: 'all', 30 | caughtErrorsIgnorePattern: '^_', 31 | destructuredArrayIgnorePattern: '^_', 32 | ignoreRestSiblings: true, 33 | varsIgnorePattern: '^_', 34 | }, 35 | ], 36 | curly: ['error', 'multi-line'], 37 | eqeqeq: ['error', 'always', { null: 'ignore' }], 38 | 'func-style': ['error', 'declaration', { allowArrowFunctions: true }], 39 | 'no-throw-literal': 'error', 40 | 'no-var': 'error', 41 | 'simple-import-sort/exports': 'error', 42 | 'simple-import-sort/imports': 'error', 43 | 'sort-keys': ['error', 'asc', { caseSensitive: true, natural: true }], 44 | }, 45 | settings: { 46 | 'import/parsers': { 47 | '@typescript-eslint/parser': ['.ts', '.tsx'], 48 | }, 49 | }, 50 | } 51 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} 6 | 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | node: ['20.x', '22.x', '23.x'] 11 | os: [ubuntu-latest] 12 | 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v4 16 | 17 | - name: Use Node ${{ matrix.node }} 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: ${{ matrix.node }} 21 | 22 | - name: Install with yarn 23 | run: corepack enable && yarn 24 | 25 | - name: Lint 26 | run: yarn lint 27 | 28 | - name: Test 29 | run: yarn test 30 | 31 | - name: Build 32 | run: yarn build 33 | -------------------------------------------------------------------------------- /.github/workflows/size.yml: -------------------------------------------------------------------------------- 1 | name: size 2 | on: [pull_request] 3 | jobs: 4 | size: 5 | runs-on: ubuntu-latest 6 | env: 7 | CI_JOB_NUMBER: 1 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: andresz1/size-limit-action@v1 11 | with: 12 | github_token: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | 6 | # yarn 7 | .yarn-integrity 8 | .pnp.* 9 | .yarn/* 10 | !.yarn/patches 11 | !.yarn/plugins 12 | !.yarn/releases 13 | !.yarn/sdks 14 | !.yarn/versions 15 | yarn-debug.log* 16 | yarn-error.log* 17 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | # yarn 5 | .yarn-integrity 6 | .pnp.* 7 | .yarn/* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "printWidth": 80, 6 | "endOfLine": "lf" 7 | } 8 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | 5 | nodeLinker: node-modules 6 | 7 | yarnPath: .yarn/releases/yarn-4.5.3.cjs 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Ben Williams 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Enwrap 2 | 3 | [![bundlephobia minzip](https://img.shields.io/bundlejs/size/enwrap)](https://bundlephobia.com/package/enwrap) 4 | [![bundlephobia tree shaking](https://badgen.net/bundlephobia/tree-shaking/enwrap)](https://bundlephobia.com/package/enwrap) 5 | [![bundlephobia dependency count](https://badgen.net/bundlephobia/dependency-count/enwrap?color=black)](https://github.com/biw/enwrap/blob/main/package.json) 6 | 7 | Enwrap is a tiny (423 bytes gzipped) and dependency-free library that allows you to wrap functions and return typed errors, with a focus on ease of use and developer experience. 8 | 9 | Unlike other libraries, Enwrap does not require you to learn a new, dramatically different syntax; most TypeScript developers will feel right at home after a few minutes. 10 | 11 | > [!IMPORTANT] 12 | > Although Enwrap is currently in multiple production codebases, Enwrap still has a few rough edges where the library has overly strict types. If you hit a rough edge or something feels more complicated to do than you think it should, please open a ticket! 13 | 14 | ## Installation 15 | 16 | ```bash 17 | yarn add enwrap 18 | ``` 19 | 20 | ## Usage 21 | 22 | Enwrap has only one function, `ew`, which takes a function and returns a fully typed function with error handling. 23 | 24 | ### Basic Example 25 | 26 | ```ts 27 | import { ew } from 'enwrap' 28 | 29 | // notice the first argument, `err`, this is a function we will call whenever 30 | // we want to return an error 31 | // any other arguments are the arguments we want to pass to the function 32 | // in this case, we want to pass a number to the function 33 | const getPositiveNumber = ew((err, num: number) => { 34 | if (num < 0) { 35 | return err('number must be positive') 36 | } 37 | return num 38 | }) 39 | 40 | const res = getPositiveNumber(1) 41 | // ^? `WithNoError | TypedError | TypedError<'number must be positive'>` 42 | 43 | // if we want to access the number, we need to check if the error is present 44 | if (res.error) { 45 | console.log(res.error) 46 | } else { 47 | console.log(res) // 1 48 | } 49 | ``` 50 | 51 | Enwrap supports returning any value from the wrapped function, and will type the value with `WithNoError`, which is a type that represents a value that is not an error. From a runtime perspective, there's nothing special about `WithNoError; it's just a wrapper type. 52 | 53 | ### Typed Error Handling 54 | 55 | One massive advantage of Enwrap, vs. `throw new Error()` is that all explicit errors are typed. This allows you to handle different types of errors in a type-safe manner & with editor autocomplete! 56 | 57 | One important thing to note is that since there's no way to type or detect errors that are thrown in a function, Enwrap includes a generic `TypedError` return type for all functions, even ones that don't explicitly return an error. 58 | 59 | ```ts 60 | const sometimesThrow = () => { 61 | if (Math.random() > 0.5) { 62 | throw new Error('this is an error') 63 | } 64 | } 65 | 66 | const getPrimeNumber = ew((err, num: number) => { 67 | if (num <= 0) { 68 | return err('number must be greater than 0') 69 | } 70 | if (num < 2) { 71 | return err('number must be greater than 1') 72 | } 73 | 74 | // If we have a function that throws an error, it will be caught and returned 75 | // as a `TypedError` 76 | sometimesThrow() 77 | 78 | // lol this is not a prime number check (but these are example docs) 79 | return num % 2 !== 0 80 | }) 81 | 82 | const is50Prime = getPrimeNumber(50) 83 | // ^? `WithNoError | TypedError | TypedError<'number must be greater than 0'> | TypedError<'number must be greater than 1'>` 84 | 85 | if (is50Prime.error?.message === 'number must be greater than 0') { 86 | // shame the number for not being greater than 0 87 | alert('shame for negative numbers') 88 | } 89 | if (is50Prime.error?.message === 'number must be greater than 1') { 90 | // Look up if 1 is a prime number on Wikipedia 91 | window.open('https://en.wikipedia.org/wiki/Prime_number', '_blank') 92 | } 93 | if (is50Prime.error) { 94 | // This is an error that we didn't expect, and we should probably log it 95 | console.error(is50Prime.error.message) 96 | // and then send off the error for debugging/sentry/logging/etc 97 | sendErrorToLoggingService(is50Prime.error) 98 | } 99 | ``` 100 | 101 | As we can see above, Enwrap will return a union of all possible errors that can occur in the function. This allows you to handle all errors in a type-safe manner. The error returned extends the base [`Error` object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error), so your existing code for debugging/sentry/logging/etc. will work without any changes. 102 | 103 | ### Error wasThrown 104 | 105 | Enwrap will set the `wasThrown` property on the error object to `true` if the error was thrown from inside the wrapped function or one of it's children. This is useful in cases where you want to handle throw errors 106 | vs. expected errors differently. 107 | 108 | ```ts 109 | const getPrimeNumber = ew((err, num: number) => { 110 | if (num <= 0) { 111 | return err('number must be greater than 0') 112 | } 113 | 114 | // .... 115 | 116 | if (Math.random() > 1) { 117 | // this will never happen, but hopefully this example is clear 118 | throw new Error('the random function is broken') 119 | } 120 | return num 121 | }) 122 | 123 | const res = getPrimeNumber(50) 124 | // ^? `WithNoError | TypedError | TypedError<'number must be greater than 0'> | TypedError<'number must be greater than 1'>` 125 | 126 | if (res.error.wasThrown) { 127 | // this is an error that was thrown from inside the wrapped function 128 | console.error(res.error.message) 129 | } else { 130 | // this is an error that was expected 131 | console.error(res.error.message) 132 | // ^? `TypedError<'number must be greater than 0'> | TypedError<'number must be greater than 1'>` 133 | } 134 | ``` 135 | 136 | ### Error Extra Data 137 | 138 | There are times when you may want to include extra context/metadata that you want to include when sending the error to error tracking services like Sentry. 139 | 140 | Enwrap allows you to do this by passing an object as the second argument to `err()` callback. 141 | 142 | ```ts 143 | const getUserName = ew(async (err, userId: number) => { 144 | const user = await database.getUser(userId) 145 | 146 | if (!user) { 147 | return err('user not found', { userId }) 148 | } 149 | 150 | return user.name 151 | }) 152 | const userName = await getUserName(1) 153 | // ^? `Promise | TypedError | TypedError<'user not found', { userId: number }>>` 154 | 155 | if (userName.error) { 156 | // the extra data is available on the error object 157 | console.error(userName.error.extraData?.userId) 158 | } 159 | ``` 160 | 161 | ### Invalid Return Types 162 | 163 | Enwrap takes an opinionated stance on error types, which allows it to provide more helpful error messages and better integration with TypeScript. However, this means that any type returned from the wrapped function must be a valid error type or non-error type. 164 | 165 | > [!CAUTION] 166 | > **You cannot return an object with a `.error` property from an Enwrap function** 167 | 168 | Enwrap is designed to prevent footguns, so anytime you try to return an object with an `.error` property, the function return type will be `never`. 169 | 170 | If you are seeing `never` as the return type of your Enwrap function, you are doing something wrong. (if you don't think you are, please open an issue) 171 | 172 | ```ts 173 | const getUser = ew((err, userId: number) => { 174 | // this is invalid, and will cause a typescript error 175 | return { error: 'this is an error' } 176 | }) 177 | 178 | const res = getUser(1) 179 | // ^? `never` 180 | ``` 181 | 182 | ### Returning Explicit Types 183 | 184 | As your TypeScript codebase grows, you may want to return predefined types from your Enwrap functions. Enwrap allows you to do this by setting the return type of the Enwrap function to the type you wish to return. 185 | 186 | > [!NOTE] 187 | > **When returning explict types, you must manually set any explicit error types.** 188 | 189 | To return an explicit type, we will use the `WithEW` helper type. 190 | 191 | ```ts 192 | import { type WithEW, ew } from 'enwrap' 193 | // a type that represents a user used in our codebase 194 | type User = { 195 | id: number 196 | name: string 197 | } 198 | 199 | // notice the return type, we are setting it to `WithEW` 200 | // no need to manually set `TypedError` 201 | const getUser = ew((err, userId: number): WithEW => { 202 | const user = database.getUser(userId) 203 | if (!user) { 204 | return err('missing user') 205 | } 206 | return user 207 | }) 208 | const user = getUser(1) 209 | // ^? `WithNoError | TypedError<'missing user'> | TypedError` 210 | ``` 211 | 212 | If we want to return extra data with our error, we can do so by passing an object 213 | as the second argument to `WithEW`: 214 | 215 | ```ts 216 | import { type WithEW, ew } from 'enwrap' 217 | 218 | const getUser = ew((err, userId: number): WithEW => { 219 | const user = database.getUser(userId) 220 | if (!user) { 221 | return err('missing user', { userId }) 222 | } 223 | return user 224 | }) 225 | 226 | const user = getUser(1) 227 | // ^? `WithNoError | TypedError<'missing user', { userId: number }> | TypedError` 228 | ``` 229 | 230 | > [!TIP] 231 | > You can also use the `GetReturnTypeErrors` helper type to get error types from a function, to make combining multiple levels of Enwrap easier. 232 | 233 | ```ts 234 | // continuing from above 235 | 236 | type GetUserErrors = GetReturnTypeErrors 237 | // ^? `TypedError<'missing user', { userId: number }> | TypedError` 238 | 239 | const getUserName = ew( 240 | async ( 241 | err, 242 | userId: number, 243 | ): WithEW => { 244 | const user = await getUser(userId) 245 | if (user.error) { 246 | return user // return the full type, not just the error 247 | } 248 | if (user.name === '') { 249 | return err('empty username') 250 | } 251 | return user.name 252 | }, 253 | ) 254 | 255 | const userName = await getUserName(1) 256 | // ^? `WithNoError | TypedError | TypedError<'empty username'> | TypedError<'missing user', { userId: number }>` 257 | ``` 258 | 259 | ## FAQ 260 | 261 | ### Does Enwrap support async functions? 262 | 263 | Yes, Enwrap supports async functions. All returns types are preserved and wrapped in a `Promise`. When using `WithEW`, the return type should be wrapped in a `Promise>`. 264 | 265 | ### Why not just use `throw` and `try/catch`? 266 | 267 | Using `throw` and `try/catch` is a valid approach to error handling, but it lacks the type safety that Enwrap provides. Enwrap intentionally takes a different approach by allowing you to keep using your existing error handling patterns, while incrementally adding more safety. You can still use `throw` and `try/catch` with Enwrap, it just won't be type safe. 268 | 269 | ### Why not use a library like `ts-results` or `neverthrow`? 270 | 271 | Enwrap is designed to be a simple, lightweight library that allows you to add typed errors to your functions without learning a new syntax. With only one main export, it is designed to be easy to add to existing codebases, incrementally adopted, and easy for developers on your team to understand. 272 | 273 | If you are looking for a library that provides a more complex error-handling system and more features, you may want to look into [`ts-results`](https://github.com/vultix/ts-results) or [`neverthrow`](https://github.com/supermacro/neverthrow). 274 | 275 | ### What kind of values can I return from an Enwrap function? 276 | 277 | Enwrap functions can return any value, including `void`, `null`, and `undefined`. 278 | 279 | ### What happens if I throw a non-error value? 280 | 281 | As you may know, you can throw any value in JavaScript/TypeScript. Enwrap will catch any value thrown from a wrapped function, and return it as a `TypedError` with the value of the thrown error as the error message. If it's a non-string value, it will be converted to a string using `String(error)`. If it's an object, it will be converted to a string using `JSON.stringify(error)`. If it's an empty string, it will be converted to the string `'e'`. 282 | 283 | Using ESLint's [`no-throw-literal`](https://eslint.org/docs/latest/rules/no-throw-literal) rule is recommended to prevent yourself from throwing non-error values. 284 | 285 | For example: 286 | 287 | ```ts 288 | const throwNumber = ew(() => { 289 | throw 123 290 | }) 291 | const res = throwNumber() 292 | // ^? TypedError 293 | 294 | console.log(res.error.message) // "123" 295 | ``` 296 | 297 | ### How can I send the error to Sentry or other error tracking services? 298 | 299 | Just send the error to the error tracking service you normally would. 300 | 301 | ```ts 302 | // ... your getUser function ... 303 | 304 | const res = getUser(1) 305 | 306 | if (res.error) { 307 | // for example, send the error to Sentry 308 | sendErrorToSentry(res.error) 309 | } 310 | ``` 311 | 312 | ### How can I get the error types returned from an Enwrap function? 313 | 314 | You can use the `GetReturnTypeErrors` helper type to get the errors from an Enwrap function. 315 | 316 | ```ts 317 | import { ew, type GetReturnTypeErrors } from 'enwrap' 318 | 319 | const getUser = ew((err, userId: number) => { 320 | // ... 321 | }) 322 | type GetUserErrors = GetReturnTypeErrors 323 | // ^? `TypedError | ...` 324 | ``` 325 | 326 | ### I think I found a bug, what should I do? 327 | 328 | Please open a [GitHub Issue](https://github.com/biw/enwrap/issues). 329 | 330 | ## License 331 | 332 | [MIT](https://github.com/biw/enwrap/blob/main/LICENSE) 333 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "enwrap", 3 | "version": "0.0.14", 4 | "description": "Tiny function wrapper that returns typed errors", 5 | "license": "MIT", 6 | "author": "Ben Williams", 7 | "repository": "https://github.com/biw/enwrap", 8 | "homepage": "https://github.com/biw/enwrap", 9 | "bugs": { 10 | "url": "https://github.com/biw/enwrap/issues" 11 | }, 12 | "exports": { 13 | ".": { 14 | "import": { 15 | "types": "./dist/index.d.mts", 16 | "default": "./dist/index.mjs" 17 | }, 18 | "require": { 19 | "types": "./dist/index.d.ts", 20 | "default": "./dist/index.js" 21 | } 22 | } 23 | }, 24 | "main": "dist/index.js", 25 | "module": "dist/index.mjs", 26 | "typings": "dist/index.d.ts", 27 | "sideEffects": false, 28 | "files": [ 29 | "dist", 30 | "src" 31 | ], 32 | "scripts": { 33 | "build": "rm -rf dist && tsup src/index.ts --format cjs,esm --dts --minify --treeshake --clean", 34 | "postpublish": "PACKAGE_VERSION=$(cat package.json | grep \\\"version\\\" | head -1 | awk -F: '{ print $2 }' | sed 's/[\",]//g' | tr -d '[[:space:]]') && git tag v$PACKAGE_VERSION && git push --tags", 35 | "lint": "tsc && eslint src test", 36 | "prepare": "yarn build", 37 | "size": "size-limit", 38 | "test": "vitest" 39 | }, 40 | "engines": { 41 | "node": ">=12" 42 | }, 43 | "size-limit": [ 44 | { 45 | "path": "dist/index.mjs", 46 | "limit": "1 KB" 47 | }, 48 | { 49 | "path": "dist/index.js", 50 | "limit": "1 KB" 51 | } 52 | ], 53 | "devDependencies": { 54 | "@size-limit/preset-small-lib": "^11.2.0", 55 | "@tsconfig/recommended": "^1.0.8", 56 | "@typescript-eslint/eslint-plugin": "8.24.1", 57 | "@typescript-eslint/parser": "8.24.1", 58 | "eslint": "8.57.1", 59 | "eslint-config-prettier": "10.0.1", 60 | "eslint-import-resolver-typescript": "3.8.2", 61 | "eslint-plugin-import": "2.31.0", 62 | "eslint-plugin-prettier": "5.2.3", 63 | "eslint-plugin-simple-import-sort": "12.1.1", 64 | "eslint-plugin-typescript-sort-keys": "3.3.0", 65 | "prettier": "3.5.1", 66 | "size-limit": "^11.2.0", 67 | "tsup": "^8.3.6", 68 | "typescript": "^5.7.3", 69 | "vite": "^6.1.1", 70 | "vitest": "^3.0.6" 71 | }, 72 | "packageManager": "yarn@4.5.3" 73 | } 74 | -------------------------------------------------------------------------------- /src/getParsedStack.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper function to get the line number of the error. 3 | * 4 | * This is useful for debugging/testing, as it allows us to see the exact line 5 | * that the error occurred on. 6 | * 7 | * **This should only be used for testing purposes and internally to enwrap, 8 | * please don't use this function directly.** 9 | */ 10 | export const getParsedStack = ( 11 | e: Error | undefined, 12 | // if we parse the error in the file, we want to pop off the first line 13 | // since the first line points to the library code, not the user's code 14 | popOffStack = true, 15 | ) => { 16 | const lines = (e?.stack?.split('\n') || []).filter((_, i) => 17 | popOffStack ? i !== 1 : true, 18 | ) 19 | 20 | const firstLine = lines[1] 21 | if (!firstLine) return null 22 | const colonSplit = firstLine.split(':') 23 | const lineNumber = colonSplit[colonSplit.length - 2] 24 | 25 | const editedStack = lines.join('\n') 26 | return [editedStack, lineNumber] as const 27 | } 28 | -------------------------------------------------------------------------------- /src/helperTypes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a helper type to make type checking easier for error handling 3 | * 4 | * While the type technically requires a space, in practice with the below 5 | * code, it's actual requirements are to make sure the string is not empty. 6 | * 7 | * For example, 'hi' is a valid NonEmptyString, but '' is not. 8 | * 9 | * **DO NOT USE THIS TYPE DIRECTLY - INTERNAL USE ONLY** 10 | */ 11 | export type NonEmptyString = `${string} ${string}` & { __nonEmptyString: never } 12 | 13 | /** 14 | * This is a internal type that is used to create the TypedError type. 15 | * 16 | * We do this so the auto-complete looks better in the IDE. 17 | */ 18 | type TypedErrorValue< 19 | M extends string extends infer J ? (J extends '' ? never : J) : never, 20 | wasThrown extends boolean = false, 21 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 22 | ExtraData extends Record | never = never, 23 | > = Omit & { 24 | __isTypedErrorValue: never 25 | message: M 26 | wasThrown: wasThrown 27 | } & ([ExtraData] extends [never] 28 | ? { extraData?: never } 29 | : keyof ExtraData extends never 30 | ? { extraData?: never } 31 | : { extraData: ExtraData }) 32 | 33 | /** 34 | * An error type that includes a non-empty string 35 | * 36 | * We don't use the extends/class setup since it will create a new error 37 | * that may break other custom errors that the end user's application will 38 | * throw. We just patch those errors 39 | */ 40 | export type TypedError< 41 | M extends string extends infer J ? (J extends '' ? never : J) : never, 42 | wasThrown extends boolean = M extends NonEmptyString ? true : false, 43 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 44 | ExtraData extends Record | never = never, 45 | > = { 46 | __isTypedError: never 47 | error: TypedErrorValue 48 | } 49 | 50 | /** 51 | * This allows us to add an extra error property to primitive types, without 52 | * casting all the primitive types as Object types and showing their prototype 53 | * methods in the IDE type description. 54 | */ 55 | export type WithNoError = T & { error?: never } 56 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { getParsedStack } from './getParsedStack' 2 | import { NonEmptyString, TypedError, WithNoError } from './helperTypes' 3 | 4 | /** 5 | * This parses the error from a try catch block, so that we can get a 6 | * more accurate error message. 7 | */ 8 | const parseTryCatchError = (error: unknown): Error => { 9 | if (error instanceof Error) return error 10 | const err = new Error(String(error).padStart(1, 'e')) 11 | if (err.message === '[object Object]') { 12 | return new Error(JSON.stringify(error)) 13 | } 14 | return err 15 | } 16 | 17 | /** 18 | * Merges types to allow for better type descriptions when the user 19 | * hovers over types in the editor. 20 | */ 21 | type Prettify = { [K in keyof T]: T[K] } & {} 22 | 23 | /** 24 | * By making the type arguments of ew const, we can do much more detailed type 25 | * checking, but if we keep the return type const, it forces the end developer 26 | * to deal with readonly properties. 27 | * 28 | * To get the best of both worlds, we make the type arguments const, but the 29 | * return type non-const with the DeepWriteable type. 30 | */ 31 | type DeepWriteable = { 32 | -readonly [P in keyof T]: T[P] extends 33 | | number 34 | | string 35 | | boolean 36 | | bigint 37 | | symbol 38 | | any[] 39 | | ((...args: never) => unknown) 40 | | Date 41 | ? T[P] 42 | : Prettify> 43 | } 44 | 45 | /** 46 | * Same as DeepWriteable, but for tuples. Allowing us to remove readonly 47 | * properties from tuples, so that the end developer doesn't have to deal with 48 | * readonly properties (if they don't want to). 49 | */ 50 | type RemoveReadonlyTuple = T extends readonly [infer U, ...infer U2] 51 | ? [U, ...U2] 52 | : T 53 | 54 | type WithNoErrorC = WithNoError> 55 | 56 | /** 57 | * This parses the return type of actual function correctly, handling 58 | * void, undefined, primitive types, and finally objects. 59 | */ 60 | type GetNonErrorTypes = [T] extends [never] 61 | ? TypedError | undefined 62 | : T extends void 63 | ? undefined 64 | : T extends null 65 | ? null 66 | : T extends 67 | | number 68 | | string 69 | | boolean 70 | | bigint 71 | | symbol 72 | | any[] 73 | | readonly any[] 74 | | readonly [any, ...any] 75 | | ((...args: never) => unknown) 76 | | Date 77 | ? WithNoError> 78 | : T extends { error: string } 79 | ? never 80 | : T extends { __isTypedError: any } 81 | ? never 82 | : T extends { __isTypedErrorValue: any } 83 | ? never 84 | : T extends { __isCustomEWReturnType?: never } 85 | ? WithNoErrorC 86 | : Prettify & { error?: never }> 87 | 88 | /** 89 | * This types all returned errors as the actual string, alongside 90 | * the NonEmptyString type (so autocomplete works as expected). 91 | */ 92 | type GetHardcodedErrors = T extends never | void 93 | ? never | undefined 94 | : T extends { __isTypedError: any } 95 | ? T 96 | : never 97 | 98 | type ClearInvalidErrorType = [J] extends [never] 99 | ? undefined 100 | : HasInvalidErrorType extends null 101 | ? J 102 | : never 103 | 104 | /** 105 | * Returns true if the error type is invalid. 106 | * A valid string is undefined, null, or a non-empty string. 107 | */ 108 | type HasInvalidErrorType = T extends { __invalidCustomEWReturnType: true } 109 | ? true 110 | : T extends { __nonEmptyString: any } 111 | ? true 112 | : T extends { __isTypedErrorValue: any } 113 | ? true 114 | : T extends { error?: any } 115 | ? T extends { __isTypedError: any } 116 | ? // eslint-disable-next-line @typescript-eslint/no-unused-vars 117 | T extends WithNoError 118 | ? T extends (...args: never) => unknown 119 | ? null 120 | : true 121 | : null 122 | : true 123 | : [keyof T] extends ['error'] 124 | ? T extends { __nonEmptyString: any } 125 | ? true 126 | : // eslint-disable-next-line @typescript-eslint/no-unused-vars 127 | T extends WithNoError 128 | ? T extends (...args: never) => unknown 129 | ? null 130 | : true 131 | : null 132 | : T extends { __isErrorCallback: true } 133 | ? true 134 | : null 135 | 136 | const errorCallback = < 137 | T extends string, 138 | ReturnT extends T extends '' ? never : T, 139 | const ExtraData extends Record | never = never, 140 | ErrorType extends TypedError< 141 | ReturnT, 142 | false, 143 | ExtraData extends never 144 | ? never 145 | : ExtraData extends undefined 146 | ? never 147 | : ExtraData extends 148 | | number 149 | | string 150 | | boolean 151 | | bigint 152 | | symbol 153 | | any[] 154 | | readonly any[] 155 | | readonly [any, ...any] 156 | | ((...args: never) => unknown) 157 | | Date 158 | ? ExtraData 159 | : Prettify> 160 | > = TypedError< 161 | ReturnT, 162 | false, 163 | ExtraData extends never 164 | ? never 165 | : ExtraData extends undefined 166 | ? never 167 | : ExtraData extends 168 | | number 169 | | string 170 | | boolean 171 | | bigint 172 | | symbol 173 | | any[] 174 | | readonly any[] 175 | | readonly [any, ...any] 176 | | ((...args: never) => unknown) 177 | | Date 178 | ? ExtraData 179 | : Prettify> 180 | >, 181 | >( 182 | msg: BlockEmptyString>, 183 | extraData?: ExtraData, 184 | ): ErrorType => { 185 | const rawError = new Error(msg) as any as TypedError< 186 | ReturnT, 187 | false, 188 | any 189 | >['error'] 190 | rawError.stack = getParsedStack(rawError)?.[0] ?? '' 191 | rawError.extraData = extraData || ({} as any) 192 | rawError.wasThrown = false 193 | return { error: rawError } as ErrorType 194 | } 195 | 196 | type ErrorCallback = typeof errorCallback & { __isErrorCallback: true } 197 | 198 | type FilterOutUsingWithEW = T extends { __isCustomEWReturnType?: any } 199 | ? Omit 200 | : T 201 | 202 | export const ew = < 203 | const args extends any[], 204 | const Ret, 205 | AwaitedRet extends Awaited, 206 | FilteredRet extends FilterOutUsingWithEW, 207 | NonErrorTypes extends GetNonErrorTypes, 208 | HardcodedErrors extends GetHardcodedErrors, 209 | ErrorTypesWithoutInvalid extends ClearInvalidErrorType, 210 | IsPromise extends [Ret] extends [AwaitedRet] ? false : true, 211 | // if we have a function that only throws and does not return anything, 212 | // we need to type check it and return a NonEmptyString since it is wrapped 213 | // and will return the error 214 | AwaitedFinalRet extends [ErrorTypesWithoutInvalid] extends [never] 215 | ? never 216 | : [AwaitedRet] extends never 217 | ? TypedError | undefined 218 | : // if the function we are trying to wrap has an invalid error type, 219 | // we mark it as a never type so the end developer knows that they need 220 | // to fix the error type 221 | // otherwise we return the set of return types we have for the function 222 | | NonErrorTypes 223 | // if the function is trying to return an key with error 224 | | (HardcodedErrors extends never | undefined 225 | ? never 226 | : HardcodedErrors) 227 | | TypedError, 228 | FinalRet extends IsPromise extends true 229 | ? Promise 230 | : AwaitedFinalRet, 231 | >( 232 | fn: (firstArgs: ErrorCallback, ...a: args) => Ret, 233 | ) => { 234 | return (...args: args): FinalRet extends infer U ? U : never => { 235 | const handleError = (e: unknown) => { 236 | const rawError = parseTryCatchError(e) as TypedError< 237 | NonEmptyString, 238 | false, 239 | any 240 | >['error'] 241 | rawError.extraData = {} 242 | rawError.wasThrown = false 243 | return { error: rawError } as any 244 | } 245 | try { 246 | if (fn.constructor.name === 'AsyncFunction') { 247 | return (fn(errorCallback as ErrorCallback, ...args) as any) 248 | .then((res: any) => res) 249 | .catch((e: unknown) => handleError(e)) 250 | } 251 | return fn(errorCallback as ErrorCallback, ...args) as any 252 | } catch (e) { 253 | return handleError(e) 254 | } 255 | } 256 | } 257 | 258 | type FromErrorStringToTypedError< 259 | T extends 260 | | string 261 | | { error: string; extraData?: Record } 262 | | TypedError, 263 | > = T extends string 264 | ? T extends '' 265 | ? never 266 | : TypedError 267 | : T extends { error: infer J } 268 | ? J extends '' 269 | ? never 270 | : T extends { __isTypedError: true } 271 | ? T 272 | : T extends { error: infer J } 273 | ? J extends string 274 | ? J extends '' 275 | ? never 276 | : T extends { extraData: infer K } 277 | ? K extends Record 278 | ? TypedError 279 | : never 280 | : TypedError 281 | : T 282 | : never 283 | : never 284 | 285 | type BlockEmptyString = T extends '' ? never : T 286 | 287 | type IsGenericString = T extends string 288 | ? string extends T 289 | ? true // `T` is exactly `string` 290 | : false // `T` is a string literal 291 | : false // `T` is not a string 292 | 293 | type BlockGenericString = 294 | IsGenericString extends false 295 | ? T 296 | : IsGenericString extends true 297 | ? never 298 | : IsGenericString extends boolean 299 | ? never 300 | : T 301 | 302 | type IsGenericErrorString = T extends { error: string } 303 | ? IsGenericString 304 | : false 305 | 306 | type BlockGenericErrorString = 307 | IsGenericErrorString extends false 308 | ? T 309 | : IsGenericErrorString extends true 310 | ? never 311 | : IsGenericErrorString extends boolean 312 | ? never 313 | : T 314 | 315 | type C = T & { __isCustomEWReturnType?: never; error?: never } 316 | 317 | export type WithEW< 318 | T, 319 | Errors extends 320 | | string 321 | | { error: string; extraData?: Record } 322 | | TypedError = NonEmptyString, 323 | ValidErrors extends FromErrorStringToTypedError< 324 | BlockGenericErrorString> 325 | > = FromErrorStringToTypedError< 326 | BlockGenericErrorString> 327 | >, 328 | > = [ValidErrors] extends [never] 329 | ? { __invalidCustomEWReturnType: true } 330 | : 331 | | (T extends void ? undefined : T extends null ? null : C) 332 | | ValidErrors 333 | | TypedError 334 | 335 | /** 336 | * A helper type to get the return type errors of a function. 337 | * 338 | * This can be useful when calling multiple levels of enwrap with explicit 339 | * return types. 340 | */ 341 | export type GetReturnTypeErrors< 342 | T extends (...args: any[]) => any, 343 | AwaitedT = Awaited>, 344 | > = AwaitedT extends TypedError ? AwaitedT : never 345 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, expectTypeOf, test } from 'vitest' 2 | 3 | import { getParsedStack } from '../src/getParsedStack' 4 | import { NonEmptyString, TypedError, WithNoError } from '../src/helperTypes' 5 | import { ew, type GetReturnTypeErrors, type WithEW } from '../src/index' 6 | 7 | const noOp = (...args: T) => args 8 | 9 | describe('ew', () => { 10 | test('adds two numbers together', () => { 11 | const res = ew((err, a: number, b: number) => { 12 | // this can never happen, but typescript doesn't know that 13 | if (Math.random() > 100) { 14 | return err('error') 15 | } 16 | return a + b 17 | }) 18 | const res2 = res(1, 2) 19 | 20 | expectTypeOf(res2).toEqualTypeOf< 21 | | WithNoError 22 | | TypedError 23 | | TypedError<'error', false> 24 | >() 25 | 26 | // @ts-expect-error can't access property before error check 27 | noOp(res2.toExponential()) 28 | if (res2.error) { 29 | expect(1).toBe(2) 30 | return 31 | } 32 | expectTypeOf(res2.error).toEqualTypeOf() 33 | const isNumber = (_: number) => {} 34 | isNumber(res2) 35 | }) 36 | 37 | test('return string', () => { 38 | const res = ew(() => { 39 | return '123' 40 | }) 41 | const res2 = res() 42 | 43 | expectTypeOf(res2).toEqualTypeOf< 44 | WithNoError | TypedError 45 | >() 46 | 47 | expect(res2).toEqual('123') 48 | }) 49 | 50 | test('return boolean', () => { 51 | const res = ew(() => { 52 | return true 53 | }) 54 | const res2 = res() 55 | 56 | if (res2.error) { 57 | expect(res2.error.wasThrown).toBe(true) 58 | } 59 | 60 | expectTypeOf(res2).toEqualTypeOf< 61 | WithNoError | TypedError 62 | >() 63 | 64 | expect(res2).toEqual(true) 65 | }) 66 | 67 | test('return undefined', () => { 68 | const res = ew(() => { 69 | return undefined 70 | }) 71 | const res2 = res() 72 | 73 | expectTypeOf(res2).toEqualTypeOf< 74 | undefined | TypedError 75 | >() 76 | 77 | if (res2?.error) { 78 | expect(1).toBe(2) 79 | return 80 | } 81 | 82 | expect(res2).toEqual(undefined) 83 | }) 84 | 85 | test('return undefined or error', () => { 86 | const res = ew((err) => { 87 | if (Math.random() > 100) { 88 | return err('error') 89 | } 90 | return undefined 91 | }) 92 | const res2 = res() 93 | 94 | expectTypeOf(res2).toEqualTypeOf< 95 | undefined | TypedError | TypedError<'error'> 96 | >() 97 | 98 | if (res2?.error) { 99 | expect(1).toBe(2) 100 | return 101 | } 102 | 103 | expect(res2).toEqual(undefined) 104 | }) 105 | 106 | test('return null', () => { 107 | const res = ew(() => { 108 | return null 109 | }) 110 | const res2 = res() 111 | 112 | expectTypeOf(res2).toEqualTypeOf>() 113 | 114 | if (res2?.error) { 115 | expect(1).toBe(2) 116 | return 117 | } 118 | 119 | expect(res2).toEqual(null) 120 | }) 121 | 122 | test('return null or error', () => { 123 | const res = ew((err) => { 124 | if (Math.random() > 100) { 125 | return err('error') 126 | } 127 | return null 128 | }) 129 | const res2 = res() 130 | 131 | expectTypeOf(res2).toEqualTypeOf< 132 | null | TypedError | TypedError<'error'> 133 | >() 134 | 135 | if (res2?.error) { 136 | expect(1).toBe(2) 137 | return 138 | } 139 | 140 | expect(res2).toEqual(null) 141 | }) 142 | 143 | test('return void', () => { 144 | const res = ew(() => { 145 | return 146 | }) 147 | const res2 = res() 148 | 149 | expectTypeOf(res2).toEqualTypeOf< 150 | undefined | TypedError 151 | >() 152 | }) 153 | 154 | test('return void or error', () => { 155 | const res = ew((err) => { 156 | if (Math.random() > 100) { 157 | return err('error') 158 | } 159 | return 160 | }) 161 | const res2 = res() 162 | 163 | expectTypeOf(res2).toEqualTypeOf< 164 | undefined | TypedError | TypedError<'error'> 165 | >() 166 | }) 167 | 168 | test('return void using WithEW', () => { 169 | const res = ew((err): WithEW => { 170 | if (Math.random() > 100) { 171 | return 172 | } 173 | if (Math.random() > 100) { 174 | return err('errorStr') 175 | } 176 | }) 177 | const res2 = res() 178 | 179 | expectTypeOf(res2).toEqualTypeOf< 180 | TypedError | TypedError<'errorStr'> | undefined 181 | >() 182 | }) 183 | 184 | test('return null using WithEW', () => { 185 | const res = ew((err): WithEW => { 186 | if (Math.random() > 100) { 187 | return err('errorStr') 188 | } 189 | return null 190 | }) 191 | const res2 = res() 192 | 193 | expectTypeOf(res2).toEqualTypeOf< 194 | null | TypedError | TypedError<'errorStr'> 195 | >() 196 | }) 197 | 198 | test('return undefined using WithEW', () => { 199 | const res = ew((err): WithEW => { 200 | if (Math.random() > 100) { 201 | return err('errorStr') 202 | } 203 | return undefined 204 | }) 205 | const res2 = res() 206 | 207 | expectTypeOf(res2).toEqualTypeOf< 208 | TypedError | TypedError<'errorStr'> | undefined 209 | >() 210 | }) 211 | 212 | test('non-return void using WithEW', () => { 213 | const res = ew((err): WithEW => { 214 | if (Math.random() > 100) { 215 | return err('error123') 216 | } 217 | noOp(1) 218 | }) 219 | const res2 = res() 220 | 221 | expectTypeOf(res2).toEqualTypeOf< 222 | TypedError<'error123'> | TypedError | undefined 223 | >() 224 | }) 225 | 226 | test('return object', () => { 227 | const res = ew(() => { 228 | return { a: 1 } 229 | }) 230 | const res2 = res() 231 | 232 | expectTypeOf(res2).toEqualTypeOf< 233 | { a: 1; error?: never } | TypedError 234 | >() 235 | 236 | if (res2.error) { 237 | expectTypeOf(res2).toHaveProperty('error') 238 | expect(1).toBe(2) 239 | return 240 | } 241 | expectTypeOf(res2.error).toEqualTypeOf() 242 | expect(res2).toEqual({ a: 1 }) 243 | }) 244 | 245 | test('try to return error object', () => { 246 | const res = ew(() => { 247 | if (Math.random() > 100) { 248 | return 123 249 | } 250 | return { error: new Error('error') } 251 | }) 252 | const res2 = res() 253 | 254 | expectTypeOf(res2).toEqualTypeOf() 255 | 256 | // @ts-expect-error can't access type on never 257 | if (res2.error) { 258 | expect(1).toBe(1) 259 | return 260 | } 261 | }) 262 | 263 | test('try to return non error string', () => { 264 | const res = ew(() => { 265 | if (Math.random() > 100) { 266 | return 123 267 | } 268 | return { error: 123, pizza: 123 } 269 | }) 270 | 271 | const res2 = res() 272 | 273 | expectTypeOf(res2).toEqualTypeOf() 274 | 275 | // @ts-expect-error can't access type on never 276 | if (res2.error) { 277 | expect(1).toBe(1) 278 | return 279 | } 280 | // @ts-expect-error can't access type on never 281 | noOp(res2.pizza) 282 | }) 283 | 284 | test('try to return only an error string', () => { 285 | const res = ew(() => { 286 | if (Math.random() > 100) { 287 | return 123 288 | } 289 | return { error: 'err2' } 290 | }) 291 | 292 | const res2 = res() 293 | // don't allow access to property without error check 294 | // @ts-expect-error can't access type on never 295 | noOp(res2.a) 296 | 297 | expectTypeOf(res2).toEqualTypeOf() 298 | }) 299 | 300 | test('try to call err with a generic error string', () => { 301 | ew((err) => { 302 | const x: string = 'error' 303 | // @ts-expect-error can't pass in a generic string 304 | return err(x) 305 | }) 306 | }) 307 | 308 | test('try to call err with a generic any type', () => { 309 | ew((err) => { 310 | const x: any = 'error' 311 | // @ts-expect-error can't pass in a generic any 312 | return err(x) 313 | }) 314 | }) 315 | 316 | test('incorrectly return err function', () => { 317 | const res = ew((err) => { 318 | if (Math.random() > 100) { 319 | return err('123') 320 | } 321 | return err 322 | }) 323 | 324 | const res2 = res() 325 | // don't allow access to property without error check 326 | // @ts-expect-error can't access type on never 327 | noOp(res2.toString()) 328 | 329 | expectTypeOf(res2).toEqualTypeOf() 330 | }) 331 | 332 | test('try to return an optional error string', () => { 333 | const res = ew(() => { 334 | // this can never happen, but typescript doesn't know that 335 | if (Math.random() > 100) { 336 | return { pizza: 123 } 337 | } 338 | return { error: 'err2' } 339 | }) 340 | 341 | const res2 = res() 342 | // don't allow access to property without error check 343 | // @ts-expect-error can't access type on never 344 | noOp(res2.a) 345 | 346 | expectTypeOf(res2).toEqualTypeOf() 347 | }) 348 | 349 | test('return a Date object', () => { 350 | const res = ew(() => { 351 | return new Date() 352 | }) 353 | const res2 = res() 354 | 355 | expectTypeOf(res2).toEqualTypeOf< 356 | WithNoError | TypedError 357 | >() 358 | }) 359 | 360 | test('return a Date object deep in an object', () => { 361 | const res = ew(() => { 362 | return { a: { b: new Date() } } 363 | }) 364 | const res2 = res() 365 | 366 | expectTypeOf(res2).toEqualTypeOf< 367 | | TypedError 368 | | { a: { b: Date }; error?: never | undefined } 369 | >() 370 | }) 371 | 372 | test('return a Date object in extra data', () => { 373 | const res = ew((err) => { 374 | return err('error', { pizza: new Date() }) 375 | }) 376 | const res2 = res() 377 | 378 | expectTypeOf(res2).toEqualTypeOf< 379 | | TypedError<'error', false, { pizza: Date }> 380 | | TypedError 381 | >() 382 | }) 383 | 384 | test('return a function', () => { 385 | const res = ew(() => { 386 | return () => { 387 | return null 388 | } 389 | }) 390 | const res2 = res() 391 | 392 | expectTypeOf(res2).toEqualTypeOf< 393 | TypedError | WithNoError<() => null> 394 | >() 395 | }) 396 | 397 | test('edit object', () => { 398 | const res = ew((_, a: { a: number }) => { 399 | return { ...a, b: 2 } 400 | }) 401 | const res2 = res({ a: 1 }) 402 | 403 | expectTypeOf(res2).toEqualTypeOf< 404 | { a: number; b: 2; error?: never } | TypedError 405 | >() 406 | 407 | if (res2.error) { 408 | // @ts-expect-error can't access non-error property 409 | noOp(res2.a) 410 | expect(1).toBe(2) 411 | noOp(res2) 412 | return 413 | } 414 | expectTypeOf(res2.error).toEqualTypeOf() 415 | noOp(res2.b) 416 | res2.a = 200 417 | expect(res2).toEqual({ a: 200, b: 2 }) 418 | }) 419 | 420 | test('return error or object', () => { 421 | const res = ew((err) => { 422 | // this can never happen, but typescript doesn't know that 423 | if (Math.random() > 100) { 424 | return { a: 123 } 425 | } 426 | return err('error') 427 | }) 428 | 429 | const res2 = res() 430 | // don't allow access to property without error check 431 | // @ts-expect-error can't access non-error property 432 | noOp(res2.a) 433 | 434 | expectTypeOf(res2).toEqualTypeOf< 435 | | TypedError<'error'> 436 | | TypedError 437 | | { a: 123; error?: never } 438 | >() 439 | 440 | if (res2.error?.message === 'error') { 441 | const hasErrorString = (_: 'error') => {} 442 | hasErrorString(res2.error.message) 443 | return 444 | } 445 | 446 | // don't allow access to property without error check 447 | // @ts-expect-error can't access non-error property 448 | noOp(res2.a) 449 | 450 | // // sadly this doesn't work in typescript (yet) 451 | // // see https://github.com/microsoft/TypeScript/issues/30506#issuecomment-474802840 452 | // https://github.com/microsoft/TypeScript/issues/31755#issuecomment-498669080 453 | // expectTypeOf(res2).toEqualTypeOf< 454 | // TypedError | { a: 123; error?: never } 455 | // >() 456 | 457 | if (res2.error) { 458 | // @ts-expect-error can't access non-error property 459 | noOp(res2.a) 460 | expect(1).toBe(2) 461 | return 462 | } 463 | expectTypeOf(res2.error).toEqualTypeOf() 464 | noOp(res2.a) 465 | }) 466 | 467 | test('no return', () => { 468 | const res = ew(() => { 469 | noOp(1) 470 | }) 471 | const res2 = res() 472 | 473 | expectTypeOf(res2).toEqualTypeOf< 474 | TypedError | undefined 475 | >() 476 | 477 | // if we don't have a return type, we need to check for nullish 478 | // vs being able to access the optional error property in order for 479 | // typescript to narrow the type 480 | if (res2) { 481 | expectTypeOf(res2).toEqualTypeOf>() 482 | expect(1).toBe(2) 483 | return 484 | } 485 | 486 | expectTypeOf(res2).toEqualTypeOf() 487 | }) 488 | 489 | test('throws error', () => { 490 | const res = ew(() => { 491 | noOp(123) 492 | throw new Error('error') 493 | }) 494 | const res2 = res() 495 | 496 | expect(getParsedStack(res2?.error, false)?.[1]).toBe('492') 497 | 498 | expectTypeOf(res2).toEqualTypeOf< 499 | TypedError | undefined 500 | >() 501 | 502 | // if we don't have a return type, we need to check for nullish 503 | // vs being able to access the optional error property in order for 504 | // typescript to narrow the type 505 | if (res2) { 506 | expectTypeOf(res2).toEqualTypeOf>() 507 | expect(res2.error.message).toBe('error') 508 | return 509 | } 510 | 511 | // For some reason, we can't get res2 down to only an undefined type 512 | // it's still `{error: NonEmptyString} | undefined` even though we've 513 | // checked for the error property above 514 | expectTypeOf(res2).toEqualTypeOf() 515 | }) 516 | 517 | test('returns empty error string', () => { 518 | const res = ew(() => { 519 | return { error: '' } 520 | }) 521 | 522 | const res2 = res() 523 | expectTypeOf(res2).toEqualTypeOf() 524 | }) 525 | 526 | test('multi-level return', () => { 527 | const res = ew(() => { 528 | return { a: { b: { c: 1 } } } 529 | }) 530 | 531 | const res2 = res() 532 | 533 | expectTypeOf(res2).toEqualTypeOf< 534 | { a: { b: { c: 1 } }; error?: never } | TypedError 535 | >() 536 | 537 | // make sure we can't access error if we don't have it 538 | // @ts-expect-error can't access error property 539 | const { a, error } = res() 540 | if (error) { 541 | expect(1).toBe(2) 542 | return 543 | } 544 | const isNumber = (_: number) => {} 545 | isNumber(a.b.c) 546 | }) 547 | 548 | test('try to return error string', () => { 549 | const res = ew(() => { 550 | // this can never happen, but typescript doesn't know that 551 | if (Math.random() > 100) { 552 | return 'error1' as NonEmptyString 553 | } 554 | return { a: 1 } 555 | }) 556 | const res2 = res() 557 | expectTypeOf(res2).toEqualTypeOf() 558 | }) 559 | 560 | test('incorrect sub-error return', () => { 561 | const res = ew((err) => { 562 | return err('deep-error') 563 | }) 564 | 565 | // if the end developer returns the error string, the return type should be 566 | // never so that they know they've made a mistake 567 | 568 | const res2 = ew(() => { 569 | const resInner = res() 570 | if (resInner.error) { 571 | return resInner.error 572 | } 573 | return { a: 1 } 574 | }) 575 | 576 | const res3 = res2() 577 | 578 | expectTypeOf(res3).toEqualTypeOf() 579 | }) 580 | 581 | test('multi-function with new object', () => { 582 | const res = ew((err) => { 583 | return err('deep-error') 584 | }) 585 | 586 | const topLevelRes = ew(() => { 587 | const resInner = res() 588 | if (resInner.error) { 589 | return resInner 590 | } 591 | // noOp(resInner.a); 592 | return { a: 1 } 593 | }) 594 | 595 | const res2 = topLevelRes() 596 | 597 | expectTypeOf(res2).toEqualTypeOf< 598 | | { a: 1; error?: never } 599 | | TypedError<'deep-error'> 600 | | TypedError 601 | >() 602 | 603 | if (res2.error) { 604 | expect(getParsedStack(res2.error, false)?.[1]).toBe('583') 605 | expectTypeOf(res2).toEqualTypeOf< 606 | TypedError | TypedError<'deep-error'> 607 | >() 608 | expect(res2.error.message).toBe('deep-error') 609 | return 610 | } 611 | 612 | expectTypeOf(res2.error).toEqualTypeOf() 613 | expectTypeOf(res2).toEqualTypeOf<{ a: 1; error?: never }>() 614 | 615 | noOp(res2.a) 616 | const isNumber = (_: number) => {} 617 | isNumber(res2.a) 618 | }) 619 | 620 | test('multi-function using WithEW', () => { 621 | const res = ew( 622 | ( 623 | err, 624 | ): WithEW< 625 | { a: number }, 626 | 'deep-error-str' | { error: 'deep-error'; extraData: { pizza: 'pie' } } 627 | > => { 628 | return err('deep-error', { pizza: 'pie' }) 629 | }, 630 | ) 631 | 632 | const res2 = res() 633 | 634 | expectTypeOf(res2).toEqualTypeOf< 635 | | TypedError<'deep-error-str'> 636 | | TypedError<'deep-error', false, { pizza: 'pie' }> 637 | | TypedError 638 | | WithNoError<{ a: number }> 639 | >() 640 | 641 | const res3 = ew( 642 | (): WithEW< 643 | { b: number }, 644 | { error: 'deep-error'; extraData: { pizza: 'pie' } } | 'deep-error-str' 645 | > => { 646 | const resInner = res() 647 | if (resInner.error) { 648 | return resInner 649 | } 650 | return { b: 1 } 651 | }, 652 | ) 653 | 654 | const res4 = res3() 655 | 656 | expectTypeOf(res4).toEqualTypeOf< 657 | | WithNoError<{ b: number }> 658 | | TypedError<'deep-error', false, { pizza: 'pie' }> 659 | | TypedError<'deep-error-str'> 660 | | TypedError 661 | >() 662 | }) 663 | 664 | test('multi-function using WithEW with attempt to throw away extra data', () => { 665 | const res = ew( 666 | ( 667 | err, 668 | ): WithEW< 669 | { a: number }, 670 | | 'deep-error-str' 671 | | 'deep-error' 672 | | { error: 'deep-error'; extraData: { pizza: 'pie' } } 673 | > => { 674 | return err('deep-error', { pizza: 'pie' }) 675 | }, 676 | ) 677 | 678 | const res2 = res() 679 | 680 | expectTypeOf(res2).toEqualTypeOf< 681 | | TypedError<'deep-error-str'> 682 | | TypedError<'deep-error'> 683 | | TypedError<'deep-error', false, { pizza: 'pie' }> 684 | | TypedError 685 | | WithNoError<{ a: number }> 686 | >() 687 | 688 | // this is a test to make sure that we can not throw away extra data 689 | // if we are returning an error 690 | const res5 = ew( 691 | (): WithEW< 692 | { b: number }, 693 | 'deep-error21' | 'deep-error' | 'deep-error-str' 694 | > => { 695 | const resInner = res() 696 | if (resInner.error) { 697 | // @ts-expect-error throwing away extra data 698 | return resInner 699 | } 700 | return { b: 1 } 701 | }, 702 | ) 703 | 704 | const res6 = res5() 705 | 706 | // the type will be valid since we trust WithEW, but typescript will 707 | // complain about the extra data being thrown away above in res5 708 | expectTypeOf(res6).toEqualTypeOf< 709 | | WithNoError<{ b: number }> 710 | | TypedError<'deep-error-str'> 711 | | TypedError<'deep-error'> 712 | | TypedError<'deep-error21'> 713 | | TypedError 714 | >() 715 | }) 716 | 717 | test('multi-function with sub-object', () => { 718 | const res = ew((err) => { 719 | return err('deep-error') 720 | }) 721 | 722 | const topLevelRes = ew(() => { 723 | const resInner = res() 724 | if (resInner.error) { 725 | return resInner 726 | } 727 | return { a: 1 } 728 | }) 729 | 730 | const res2 = topLevelRes() 731 | 732 | expectTypeOf(res2).toEqualTypeOf< 733 | | { a: 1; error?: never } 734 | | TypedError 735 | | TypedError<'deep-error'> 736 | >() 737 | 738 | if (res2.error) { 739 | expectTypeOf(res2).toEqualTypeOf< 740 | TypedError | TypedError<'deep-error'> 741 | >() 742 | expect(getParsedStack(res2.error, false)?.[1]).toBe('719') 743 | expect(res2.error.message).toBe('deep-error') 744 | return 745 | } 746 | 747 | expectTypeOf(res2.error).toEqualTypeOf() 748 | expectTypeOf(res2).toEqualTypeOf<{ a: 1; error?: never }>() 749 | 750 | noOp(res2.a) 751 | const isNumber = (_: number) => {} 752 | isNumber(res2.a) 753 | }) 754 | 755 | test('return tuple', () => { 756 | const res = ew((err) => { 757 | // this can never happen, but typescript doesn't know that 758 | if (Math.random() > 100) { 759 | return err('math-random-error') 760 | } 761 | return [1, 2, 'hey'] 762 | }) 763 | 764 | const res2 = res() 765 | expectTypeOf(res2).toEqualTypeOf< 766 | | WithNoError<[1, 2, 'hey']> 767 | | TypedError<'math-random-error'> 768 | | TypedError 769 | >() 770 | 771 | if (res2.error) { 772 | expect(1).toBe(2) 773 | return 774 | } 775 | 776 | expectTypeOf(res2).toEqualTypeOf>() 777 | }) 778 | 779 | test('return array', () => { 780 | const res = ew(() => { 781 | return Array.from({ length: 10 }, (_, i) => i) 782 | }) 783 | 784 | const res2 = res() 785 | expectTypeOf(res2).toEqualTypeOf< 786 | WithNoError | TypedError 787 | >() 788 | 789 | if (res2.error) { 790 | expect(1).toBe(2) 791 | return 792 | } 793 | 794 | expectTypeOf(res2).toEqualTypeOf>() 795 | }) 796 | 797 | test('allow settings explicit return type with error string', () => { 798 | // it's import to notice that we are setting any explicit error return 799 | // types, but we do not need to set the generic TypedError 800 | // type, which is nice 801 | type ReturnType = { pizza: 'very good' | 'bad'; x: number } 802 | const res = ew( 803 | ( 804 | err, 805 | num: number, 806 | ): WithEW< 807 | ReturnType, 808 | { error: "pizza doesn't exist"; extraData: { x: number } } 809 | > => { 810 | // this can never happen, but typescript doesn't know that 811 | if (Math.random() > 100) { 812 | return err("pizza doesn't exist", { x: 1 }) 813 | } 814 | return { pizza: 'very good', x: num } 815 | }, 816 | ) 817 | 818 | const res2 = res(1) 819 | expectTypeOf(res2).toEqualTypeOf< 820 | | WithNoError<{ pizza: 'very good' | 'bad'; x: number }> 821 | | TypedError 822 | | TypedError<"pizza doesn't exist", false, { x: number }> 823 | >() 824 | 825 | if (res2.error) { 826 | expect(1).toBe(2) 827 | return 828 | } 829 | 830 | expect(res2.pizza).toBe('very good') 831 | }) 832 | 833 | test('allow settings explicit return type with error object - no extra data', () => { 834 | // it's import to notice that we are setting any explicit error return 835 | // types, but we do not need to set the generic TypedError 836 | // type, which is nice 837 | type ReturnType = { 838 | deep: { x: string } 839 | pizza: 'very good' | 'bad' 840 | x: number 841 | } 842 | const res = ew( 843 | ( 844 | err, 845 | num: number, 846 | ): WithEW => { 847 | // this can never happen, but typescript doesn't know that 848 | if (Math.random() > 100) { 849 | return err("pizza doesn't exist") 850 | } 851 | return { deep: { x: 'hey' }, pizza: 'very good', x: num } 852 | }, 853 | ) 854 | 855 | const res2 = res(1) 856 | expectTypeOf(res2).toEqualTypeOf< 857 | | WithNoError<{ 858 | deep: { x: string } 859 | pizza: 'very good' | 'bad' 860 | x: number 861 | }> 862 | | TypedError 863 | | TypedError<"pizza doesn't exist"> 864 | >() 865 | 866 | if (res2.error) { 867 | expect(1).toBe(2) 868 | return 869 | } 870 | 871 | res2.pizza = 'bad' 872 | res2.deep.x = 'bad' 873 | 874 | expect(res2.pizza).toBe('bad') 875 | expect(res2.deep.x).toBe('bad') 876 | }) 877 | 878 | test('allow settings explicit readonly return type with error object - with extra data', () => { 879 | // one important but to notice is that we can't control 880 | type ReturnType = Readonly<{ 881 | d2: readonly [string, number] 882 | deep: Readonly<{ x: string }> 883 | pizza: 'very good' | 'bad' 884 | x: number 885 | }> 886 | 887 | const res = ew( 888 | ( 889 | err, 890 | num: number, 891 | ): WithEW< 892 | ReturnType, 893 | { error: "pizza doesn't exist"; extraData: { x: number } } 894 | > => { 895 | // this can never happen, but typescript doesn't know that 896 | if (Math.random() > 100) { 897 | return err("pizza doesn't exist", { x: num }) 898 | } 899 | return { 900 | d2: ['hey', 1], 901 | deep: { x: 'hey' }, 902 | pizza: 'very good', 903 | x: num, 904 | } 905 | }, 906 | ) 907 | 908 | const res2 = res(1) 909 | expectTypeOf(res2).toEqualTypeOf< 910 | | WithNoError< 911 | Readonly<{ 912 | d2: readonly [string, number] 913 | deep: Readonly<{ x: string }> 914 | pizza: 'very good' | 'bad' 915 | x: number 916 | }> 917 | > 918 | | TypedError 919 | | TypedError<"pizza doesn't exist", false, { x: number }> 920 | >() 921 | 922 | if (res2.error) { 923 | expect(1).toBe(2) 924 | return 925 | } 926 | 927 | expectTypeOf(res2).toEqualTypeOf< 928 | WithNoError< 929 | Readonly<{ 930 | d2: readonly [string, number] 931 | deep: Readonly<{ x: string }> 932 | pizza: 'very good' | 'bad' 933 | x: number 934 | }> 935 | > 936 | >() 937 | 938 | // @ts-expect-error can't assign to read-only property 939 | res2.pizza = 'bad' 940 | // @ts-expect-error can't assign to read-only property 941 | res2.deep.x = 'bad' 942 | 943 | // we don't control that the value is immutable, only that it's readonly 944 | // if someone changes the value, we can't do anything about it 945 | expect(res2.pizza).toBe('bad') 946 | expect(res2.deep.x).toBe('bad') 947 | }) 948 | 949 | test('WithEW - async', async () => { 950 | const res = ew( 951 | async (err): Promise>> => { 952 | return err('error') 953 | }, 954 | ) 955 | 956 | const res2 = await res() 957 | 958 | if (res2.error) { 959 | expect(1).toBe(1) 960 | } 961 | 962 | expectTypeOf(res2).toEqualTypeOf< 963 | | WithNoError> 964 | | TypedError<'error'> 965 | | TypedError 966 | >() 967 | }) 968 | 969 | test('allow returning extra data with the error', () => { 970 | const res = ew((err) => { 971 | return err('error', { thisIsAnItemOfExtraData: 123 }) 972 | }) 973 | 974 | const res2 = res() 975 | 976 | expectTypeOf(res2.error.extraData).toEqualTypeOf< 977 | { thisIsAnItemOfExtraData: 123 } | undefined 978 | >() 979 | 980 | expectTypeOf(res2).toEqualTypeOf< 981 | | TypedError<'error', false, { thisIsAnItemOfExtraData: 123 }> 982 | | TypedError 983 | >() 984 | 985 | if (res2.error.message === 'error') { 986 | expect(res2.error.extraData.thisIsAnItemOfExtraData).toBe(123) 987 | return 988 | } 989 | }) 990 | 991 | test('allow returning extra data with the error multi-level', () => { 992 | const res = ew((err) => { 993 | return err('error', { userID: 123 }) 994 | }) 995 | 996 | const res2 = ew((_) => { 997 | const resInner = res() 998 | if (resInner.error) { 999 | return resInner 1000 | } 1001 | return { a: 1 } 1002 | }) 1003 | 1004 | const res3 = res2() 1005 | expectTypeOf(res3).toEqualTypeOf< 1006 | | { a: 1; error?: never } 1007 | | TypedError 1008 | | TypedError<'error', false, { userID: 123 }> 1009 | >() 1010 | 1011 | if (res3.error?.message === 'error') { 1012 | expect(res3.error.extraData.userID).toBe(123) 1013 | return 1014 | } 1015 | }) 1016 | 1017 | test('allow async functions', async () => { 1018 | const res = ew(async (err, a: number, b: number) => { 1019 | // this can never happen, but typescript doesn't know that 1020 | if (Math.random() > 100) { 1021 | return err('error') 1022 | } 1023 | return a + b 1024 | }) 1025 | const res2 = res(1, 2) 1026 | 1027 | expectTypeOf(res2).toEqualTypeOf< 1028 | Promise< 1029 | | WithNoError 1030 | | TypedError 1031 | | TypedError<'error'> 1032 | > 1033 | >() 1034 | 1035 | const res3 = await res2 1036 | 1037 | expectTypeOf(res3).toEqualTypeOf< 1038 | | WithNoError 1039 | | TypedError 1040 | | TypedError<'error'> 1041 | >() 1042 | 1043 | // @ts-expect-error can't access property before error check 1044 | noOp(res3.toExponential()) 1045 | if (res3.error) { 1046 | expect(1).toBe(2) 1047 | return 1048 | } 1049 | expectTypeOf(res3.error).toEqualTypeOf() 1050 | const isNumber = (_: number) => {} 1051 | isNumber(res3) 1052 | }) 1053 | 1054 | test('throws async error', async () => { 1055 | const res = ew(async () => { 1056 | noOp(123) 1057 | throw new Error('error') 1058 | }) 1059 | const res2 = res() 1060 | 1061 | expectTypeOf(res2).toEqualTypeOf< 1062 | Promise | undefined> 1063 | >() 1064 | 1065 | const res3 = await res2 1066 | 1067 | expectTypeOf(res3).toEqualTypeOf< 1068 | TypedError | undefined 1069 | >() 1070 | 1071 | expect(getParsedStack(res3?.error, false)?.[1]).toBe('1057') 1072 | 1073 | // if we don't have a return type, we need to check for nullish 1074 | // vs being able to access the optional error property in order for 1075 | // typescript to narrow the type 1076 | if (res3) { 1077 | expectTypeOf(res3).toEqualTypeOf>() 1078 | expect(res3.error.message).toBe('error') 1079 | return 1080 | } 1081 | 1082 | // For some reason, we can't get res2 down to only an undefined type 1083 | // it's still `{error: NonEmptyString} | undefined` even though we've 1084 | // checked for the error property above 1085 | expectTypeOf(res3).toEqualTypeOf() 1086 | }) 1087 | 1088 | test('throw string', () => { 1089 | const res = ew(() => { 1090 | // eslint-disable-next-line no-throw-literal 1091 | throw 'error' 1092 | }) 1093 | 1094 | const res2 = res() 1095 | 1096 | if (res2?.error) { 1097 | expect(res2.error.message).toBe('error') 1098 | return 1099 | } 1100 | expect(1).toBe(2) 1101 | }) 1102 | 1103 | test('throw object', () => { 1104 | const res = ew(() => { 1105 | // eslint-disable-next-line no-throw-literal 1106 | throw { error: 'error' } 1107 | }) 1108 | 1109 | const res2 = res() 1110 | 1111 | if (res2?.error) { 1112 | expect(res2.error.message).toBe('{"error":"error"}') 1113 | return 1114 | } 1115 | expect(1).toBe(2) 1116 | }) 1117 | 1118 | test('throw number', () => { 1119 | const res = ew(() => { 1120 | // eslint-disable-next-line no-throw-literal 1121 | throw 123 1122 | }) 1123 | 1124 | const res2 = res() 1125 | 1126 | if (res2?.error) { 1127 | expect(res2.error.message).toBe('123') 1128 | return 1129 | } 1130 | expect(1).toBe(2) 1131 | }) 1132 | 1133 | test('throw boolean', () => { 1134 | const res = ew(() => { 1135 | // eslint-disable-next-line no-throw-literal 1136 | throw true 1137 | }) 1138 | 1139 | const res2 = res() 1140 | 1141 | if (res2?.error) { 1142 | expect(res2.error.message).toBe('true') 1143 | return 1144 | } 1145 | expect(1).toBe(2) 1146 | }) 1147 | 1148 | test('throw undefined', () => { 1149 | const res = ew(() => { 1150 | // eslint-disable-next-line no-throw-literal 1151 | throw undefined 1152 | }) 1153 | 1154 | const res2 = res() 1155 | 1156 | if (res2?.error) { 1157 | expect(res2.error.message).toBe('undefined') 1158 | return 1159 | } 1160 | expect(1).toBe(2) 1161 | }) 1162 | 1163 | test('throw empty string', () => { 1164 | const res = ew(() => { 1165 | // eslint-disable-next-line no-throw-literal 1166 | throw '' 1167 | }) 1168 | 1169 | const res2 = res() 1170 | 1171 | if (res2?.error) { 1172 | expect(res2.error.message).toBe('e') 1173 | return 1174 | } 1175 | expect(1).toBe(2) 1176 | }) 1177 | }) 1178 | 1179 | describe('GetReturnTypeErrors', () => { 1180 | test('basic', () => { 1181 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 1182 | const res = ew(() => { 1183 | // eslint-disable-next-line no-throw-literal 1184 | throw 1 1185 | }) 1186 | 1187 | type x = GetReturnTypeErrors 1188 | 1189 | expectTypeOf().toEqualTypeOf>() 1190 | }) 1191 | 1192 | test('with custom error - no extra data', () => { 1193 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 1194 | const res = ew((err) => { 1195 | return err('error') 1196 | }) 1197 | 1198 | type ErrorReturnType = GetReturnTypeErrors 1199 | 1200 | expectTypeOf().toEqualTypeOf< 1201 | TypedError<'error'> | TypedError 1202 | >() 1203 | }) 1204 | 1205 | test('with custom error - with extra data', () => { 1206 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 1207 | const res = ew((err) => { 1208 | return err('error', { userID: 123 }) 1209 | }) 1210 | 1211 | type ErrorReturnType = GetReturnTypeErrors 1212 | 1213 | expectTypeOf().toEqualTypeOf< 1214 | | TypedError<'error', false, { userID: 123 }> 1215 | | TypedError 1216 | >() 1217 | }) 1218 | 1219 | test('with string return type', () => { 1220 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 1221 | const res = ew(() => { 1222 | return 'pizza' 1223 | }) 1224 | 1225 | type ErrorReturnType = GetReturnTypeErrors 1226 | 1227 | expectTypeOf().toEqualTypeOf< 1228 | TypedError 1229 | >() 1230 | }) 1231 | 1232 | test('with array return type', () => { 1233 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 1234 | const res = ew(() => { 1235 | return [1, 2, 3] 1236 | }) 1237 | 1238 | type ErrorReturnType = GetReturnTypeErrors 1239 | 1240 | expectTypeOf().toEqualTypeOf< 1241 | TypedError 1242 | >() 1243 | }) 1244 | 1245 | test('with object return type', () => { 1246 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 1247 | const res = ew(() => { 1248 | return { pizza: 'very good' } 1249 | }) 1250 | 1251 | type ErrorReturnType = GetReturnTypeErrors 1252 | 1253 | expectTypeOf().toEqualTypeOf< 1254 | TypedError 1255 | >() 1256 | }) 1257 | 1258 | test('with no return type', () => { 1259 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 1260 | const res = ew(() => { 1261 | const x = 1 1262 | noOp(x) 1263 | }) 1264 | 1265 | type ErrorReturnType = GetReturnTypeErrors 1266 | 1267 | expectTypeOf().toEqualTypeOf< 1268 | TypedError 1269 | >() 1270 | }) 1271 | 1272 | test('with promise return type', () => { 1273 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 1274 | const res = ew(async (err) => { 1275 | if (Math.random() > 100) { 1276 | return err('error123', { x: 123 }) 1277 | } 1278 | if (Math.random() > 100) { 1279 | return err('error456') 1280 | } 1281 | return 'pizza' 1282 | }) 1283 | 1284 | type ErrorReturnType = GetReturnTypeErrors 1285 | 1286 | expectTypeOf().toEqualTypeOf< 1287 | | TypedError 1288 | | TypedError<'error123', false, { x: 123 }> 1289 | | TypedError<'error456'> 1290 | >() 1291 | }) 1292 | 1293 | test('use a sub-error', () => { 1294 | const res = ew((err) => { 1295 | return err('error', { userID: 123 }) 1296 | }) 1297 | 1298 | type ErrorReturnType = GetReturnTypeErrors 1299 | 1300 | expectTypeOf().toEqualTypeOf< 1301 | | TypedError<'error', false, { userID: 123 }> 1302 | | TypedError 1303 | >() 1304 | 1305 | const res2 = ew( 1306 | (err): WithEW<{ b: 1 }, ErrorReturnType | TypedError<'error'>> => { 1307 | const resInner = res() 1308 | if (resInner.error) { 1309 | return resInner 1310 | } 1311 | if (Math.random() > 100) { 1312 | return err('error') 1313 | } 1314 | if (Math.random() > 100) { 1315 | return err('error', { userID: 123 }) 1316 | } 1317 | if (Math.random() > 100) { 1318 | // @ts-expect-error invalid extra data 1319 | return err('error', { userID: 456 }) 1320 | } 1321 | return { b: 1 } 1322 | }, 1323 | ) 1324 | 1325 | const res3 = res2() 1326 | 1327 | expectTypeOf(res3).toEqualTypeOf< 1328 | | WithNoError<{ b: 1 }> 1329 | | ErrorReturnType 1330 | | TypedError<'error', false, { userID: 123 }> 1331 | | TypedError<'error'> 1332 | >() 1333 | 1334 | type errorReturnType2 = GetReturnTypeErrors 1335 | 1336 | expectTypeOf().toEqualTypeOf< 1337 | | ErrorReturnType 1338 | | TypedError<'error'> 1339 | | TypedError<'error', false, { userID: 123 }> 1340 | >() 1341 | }) 1342 | 1343 | test('WithEW returning extra data and string errors', () => { 1344 | const res = ew( 1345 | ( 1346 | err, 1347 | ): WithEW< 1348 | { a: 1 }, 1349 | 'error 123' | { error: 'error'; extraData: { userID: 123 } } 1350 | > => { 1351 | if (Math.random() > 100) { 1352 | return err('error', { userID: 123 }) 1353 | } 1354 | if (Math.random() > 100) { 1355 | return err('error 123') 1356 | } 1357 | return { a: 1 } 1358 | }, 1359 | ) 1360 | 1361 | type ErrorReturnType = GetReturnTypeErrors 1362 | 1363 | expectTypeOf().toEqualTypeOf< 1364 | | TypedError<'error', false, { userID: 123 }> 1365 | | TypedError<'error 123'> 1366 | | TypedError 1367 | >() 1368 | 1369 | const res2 = res() 1370 | 1371 | expectTypeOf(res2).toEqualTypeOf< 1372 | | WithNoError<{ a: 1 }> 1373 | | TypedError 1374 | | TypedError<'error 123'> 1375 | | TypedError<'error', false, { userID: 123 }> 1376 | >() 1377 | }) 1378 | 1379 | test('withEW without the error type', () => { 1380 | const res = ew((err): WithEW<{ a: 1 }> => { 1381 | if (Math.random() > 100) { 1382 | // @ts-expect-error can't return an error if WithEW doesn't have an error type 1383 | return err('crazy-error') 1384 | } 1385 | return { a: 1 } 1386 | }) 1387 | 1388 | const res2 = res() 1389 | 1390 | expectTypeOf(res2).toEqualTypeOf< 1391 | WithNoError<{ a: 1 }> | TypedError 1392 | >() 1393 | }) 1394 | 1395 | test('withEW invalid string error type', () => { 1396 | const res = ew((err): WithEW<{ a: 1 }, string> => { 1397 | if (Math.random() > 100) { 1398 | // @ts-expect-error the return type is now never due to the string error type 1399 | return err('error') 1400 | } 1401 | // @ts-expect-error the return type is now never due to the string error type 1402 | return { a: 1 } 1403 | }) 1404 | 1405 | const res2 = res() 1406 | 1407 | expectTypeOf(res2).toEqualTypeOf() 1408 | 1409 | type ErrorReturnType = GetReturnTypeErrors 1410 | 1411 | expectTypeOf().toEqualTypeOf() 1412 | }) 1413 | 1414 | test('withEW invalid string error type', () => { 1415 | const res = ew((err): WithEW<{ a: 1 }, any> => { 1416 | if (Math.random() > 100) { 1417 | // @ts-expect-error the return type is now never due to the string error type 1418 | return err('error') 1419 | } 1420 | // @ts-expect-error the return type is now never due to the string error type 1421 | return { a: 1 } 1422 | }) 1423 | 1424 | const res2 = res() 1425 | 1426 | expectTypeOf(res2).toEqualTypeOf() 1427 | 1428 | type ErrorReturnType = GetReturnTypeErrors 1429 | 1430 | expectTypeOf().toEqualTypeOf() 1431 | }) 1432 | 1433 | test('withEW invalid string error object type', () => { 1434 | const res = ew((err): WithEW<{ a: 1 }, { error: string }> => { 1435 | if (Math.random() > 100) { 1436 | // @ts-expect-error the return type is now never due to the string error type 1437 | return err('error') 1438 | } 1439 | // @ts-expect-error the return type is now never due to the string error type 1440 | return { a: 1 } 1441 | }) 1442 | 1443 | const res2 = res() 1444 | 1445 | expectTypeOf(res2).toEqualTypeOf() 1446 | 1447 | type ErrorReturnType = GetReturnTypeErrors 1448 | 1449 | expectTypeOf().toEqualTypeOf() 1450 | }) 1451 | 1452 | test('withEW invalid any error object type', () => { 1453 | const res = ew((err): WithEW<{ a: 1 }, { error: any }> => { 1454 | if (Math.random() > 100) { 1455 | // @ts-expect-error the return type is now never due to the string error type 1456 | return err('error') 1457 | } 1458 | // @ts-expect-error the return type is now never due to the string error type 1459 | return { a: 1 } 1460 | }) 1461 | 1462 | const res2 = res() 1463 | 1464 | expectTypeOf(res2).toEqualTypeOf() 1465 | 1466 | type ErrorReturnType = GetReturnTypeErrors 1467 | 1468 | expectTypeOf().toEqualTypeOf() 1469 | }) 1470 | 1471 | test('return type ignores all empty error strings', () => { 1472 | ew((err) => { 1473 | // @ts-expect-error can't use an empty error string 1474 | return err('') 1475 | }) 1476 | }) 1477 | 1478 | test('withEW fail with never error return type', () => { 1479 | const res = ew((): WithEW<{ a: 1 }, never> => { 1480 | // @ts-expect-error can't return a never error type 1481 | return 234 1482 | }) 1483 | 1484 | const res2 = res() 1485 | 1486 | expectTypeOf(res2).toEqualTypeOf() 1487 | }) 1488 | 1489 | test('withEW ignore all empty error strings', () => { 1490 | const res = ew((): WithEW<{ a: 1 }, ''> => { 1491 | // @ts-expect-error can't return a never error type 1492 | return 234 1493 | }) 1494 | 1495 | const res2 = res() 1496 | 1497 | expectTypeOf(res2).toEqualTypeOf() 1498 | }) 1499 | 1500 | test('withEW ignore all empty error strings', () => { 1501 | const res = ew((): WithEW<{ a: 1 }, { error: '' }> => { 1502 | // @ts-expect-error can't return a never error type 1503 | return 234 1504 | }) 1505 | 1506 | const res2 = res() 1507 | 1508 | expectTypeOf(res2).toEqualTypeOf() 1509 | }) 1510 | 1511 | test('WithEW explicit function type annotation', () => { 1512 | type RetType = (x: string) => WithEW<{ 1513 | a: { num: string }[] 1514 | b: string 1515 | }> 1516 | 1517 | const func: RetType = ew((err, x) => { 1518 | if (x === 'pizza') { 1519 | return { 1520 | a: [{ num: '123' }], 1521 | b: '123', 1522 | } 1523 | } 1524 | return { 1525 | a: [{ num: '123' }], 1526 | b: '123', 1527 | } 1528 | }) 1529 | 1530 | const res = func('pizza') 1531 | 1532 | if (res.error) { 1533 | expect(1).toEqual(2) 1534 | } 1535 | 1536 | expectTypeOf(res).toEqualTypeOf< 1537 | WithEW<{ a: { num: string }[]; b: string }> 1538 | >() 1539 | 1540 | expectTypeOf(res).toEqualTypeOf< 1541 | | ({ a: { num: string }[]; b: string } & { 1542 | __isCustomEWReturnType?: never 1543 | error?: never 1544 | }) 1545 | | TypedError 1546 | >() 1547 | }) 1548 | 1549 | test('explicit function type annotation w/ cast', () => { 1550 | type RetType = () => WithEW< 1551 | { a: { num: string }[]; b: string }, 1552 | 'general-error' 1553 | > 1554 | 1555 | // in version 0.0.13 and below, the return type of `RetType` 1556 | // plus casting any field on an object causes enwrap to 1557 | // ignore the fact that the return type was incorrectly typed 1558 | // and missing the `b` field 1559 | // @ts-expect-error ^^^ 1560 | const func: RetType = ew((err) => { 1561 | if (Math.random() > 100) { 1562 | return err('general-error') 1563 | } 1564 | return { 1565 | a: [{ num: '123' as string }] as any, 1566 | } 1567 | }) 1568 | 1569 | const res = func() 1570 | 1571 | if (res.error) { 1572 | expect(1).toEqual(2) 1573 | } 1574 | 1575 | expectTypeOf(res).toEqualTypeOf< 1576 | WithEW<{ a: { num: string }[]; b: string }, 'general-error'> 1577 | >() 1578 | 1579 | expectTypeOf(res).toEqualTypeOf< 1580 | | ({ a: { num: string }[]; b: string } & { 1581 | __isCustomEWReturnType?: never 1582 | error?: never 1583 | }) 1584 | | TypedError<'general-error'> 1585 | | TypedError 1586 | >() 1587 | }) 1588 | }) 1589 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/recommended/tsconfig.json", 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "target": "ES2017", 6 | "allowImportingTsExtensions": false, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "noFallthroughCasesInSwitch": true, 14 | "exactOptionalPropertyTypes": true, 15 | "moduleResolution": "node", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "noUncheckedIndexedAccess": true 19 | } 20 | } 21 | --------------------------------------------------------------------------------