├── .github └── workflows │ ├── cd.yml │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── build ├── set-version.sh ├── tsconfig.json ├── update-readme.sh └── webpack.config.js ├── package-lock.json ├── package.json ├── src ├── Array.ts ├── Boolean.ts ├── Number.ts ├── Object.ts ├── String.ts └── index.ts └── tests ├── Array.test.ts ├── Boolean.test.ts ├── Number.test.ts ├── Object.test.ts ├── String.test.ts └── jest.config.js /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: 14 15 | - run: npm install 16 | - id: publish 17 | uses: JS-DevTools/npm-publish@v1 18 | with: 19 | token: ${{secrets.NPM_TOKEN}} 20 | - if: steps.publish.type != 'none' 21 | run: | 22 | echo "Version changed: ${{ steps.publish.outputs.old-version }} => ${{ steps.publish.outputs.version }}" -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | 7 | jobs: 8 | test: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | node-version: [14.x] 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | - run: npm ci 23 | - run: npm test 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | _bundles 3 | lib 4 | *.swp 5 | test.js 6 | test.ts 7 | .idea 8 | tmp 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kyle Nazario 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 | # TypeSwift 2 | 3 | Swift offers a number of convenience variables and functions across its core types. When I went back to TypeScript after 4 | a summer of Swift projects, I missed a lot of these conveniences. So I've added them to TypeScript and JavaScript. 5 | 6 | ## Installation 7 | 8 | `npm install typeswift` 9 | 10 | `yarn add typeswift` 11 | 12 | ## Usage 13 | 14 | For Node and single-page applications, import TypeSwift at the start of your index file. 15 | 16 | ```js 17 | // index.js 18 | const TypeSwift = require('typeswift'); 19 | 20 | // rest of program... 21 | ``` 22 | 23 | ```typescript 24 | // index.ts 25 | import 'typeswift'; 26 | 27 | // rest of program... 28 | ``` 29 | 30 | TypeSwift also comes bundled as minified and non-minified scripts. Add one of these in HTML to use them in subsequent 31 | ` 38 | 43 | ``` 44 | 45 | Once TypeSwift is loaded, you can begin using its extensions to JS's base classes: 46 | 47 | ```typescript 48 | const nums = ['one', 'two', 'three']; 49 | console.log(nums.first); // 'one' 50 | console.log(nums[0]); // 'one' 51 | console.log(nums.last) // 'three' 52 | console.log(nums.randomElement()) // 'two' 53 | 54 | const objectsMayHaveId = [{id: 1}, {}, {id: 3}]; 55 | const ids = objectsMayHaveId.compactMap(object => object.id); 56 | console.log(ids); // [1, 3] 57 | 58 | const objectA = {one: 1, two: 2}; 59 | const objectB = {two: 'two', three: 'three'}; 60 | const merged = objectA.merging(objectB, (objA, objB) => objB); 61 | console.log(merged); // {one: 1, two: 'two', three: 'three'} 62 | 63 | const letters = 'abc'; 64 | console.log(letters.reversed()) // 'cba' 65 | console.log(letters.shuffled()) // 'cab' 66 | console.log(letters.endIndex) // 2 67 | ``` 68 | 69 | ## Features 70 | 71 | TypeSwift includes a number of convenience methods based on their Swift counterparts. It does not include: 72 | 73 | - Functionality around managing memory 74 | - Functions that mutate primitive types, since [JS primitives are immutable][1] (e.x. you can't `.toggle()` booleans) 75 | - Functions with an extremely close equivalent in JavaScript (e.x. `first(where: )` is just `find()`) 76 | 77 | [1]: https://developer.mozilla.org/en-US/docs/Glossary/Primitive 78 | 79 | ### API 80 | 81 | ```typescript 82 | type UniquingKeysCallback = (valueOne: any, valueTwo: any) => any; 83 | type KeyedObject = {[key: string]: any}; 84 | type KeyValueAssertionCallback = (key: string | number, value: E) => boolean; 85 | 86 | declare global { 87 | interface Object { 88 | readonly isEmpty: boolean; 89 | readonly count: number; 90 | readonly first: any; 91 | readonly randomElement: () => any; 92 | readonly merge: (objectToMerge: KeyedObject, uniquingKeysWith: UniquingKeysCallback) => void; 93 | readonly merging: (objectToMerge: KeyedObject, uniquingKeysWith: UniquingKeysCallback) => KeyedObject; 94 | readonly removeValue: (forKey: string | number) => void; 95 | readonly removeAll: () => void; 96 | readonly contains: (where: KeyValueAssertionCallback) => boolean; 97 | readonly allSatisfy: (callback: KeyValueAssertionCallback) => boolean; 98 | } 99 | } 100 | ``` 101 | 102 | ```typescript 103 | declare global { 104 | interface Array { 105 | readonly first: T | undefined; 106 | readonly last: T | undefined; 107 | readonly isEmpty: boolean; 108 | readonly randomElement: () => T | undefined; 109 | readonly insert: (element: T, at: number) => void; 110 | readonly remove: (at: number) => T | undefined; 111 | readonly compactMap: (callback: (element: T, index?: number, parent?: Array) => any | undefined) => Array; 112 | readonly shuffle: () => void; 113 | readonly shuffled: () => Array; 114 | readonly swapAt: (indexA: number, indexB: number) => void; 115 | readonly startIndex: number | undefined; 116 | readonly endIndex: number | undefined; 117 | } 118 | } 119 | ``` 120 | 121 | ```typescript 122 | declare global { 123 | interface String { 124 | readonly isEmpty: boolean; 125 | readonly inserted: (substring: string, at: number) => string; 126 | readonly first: string | undefined; 127 | readonly last: string | undefined; 128 | readonly randomElement: () => string | undefined; 129 | readonly map: (callback: (char: string, index?: number, parent?: string) => string) => string; 130 | readonly compactMap: (callback: (char: string, index?: number, parent?: string) => string | undefined) => string; 131 | readonly forEach: (callback: (char: string, index?: number, parent?: string) => void) => void; 132 | readonly reduce: (callback: (result: R, char: string, index?: number) => R, initialValue: R) => R; 133 | readonly sorted: () => string; 134 | readonly reversed: () => string; 135 | readonly shuffled: () => string; 136 | readonly startIndex: number | undefined; 137 | readonly endIndex: number | undefined; 138 | readonly prefix: (callback: (char: string, index?: number, parent?: string) => boolean) => string; 139 | } 140 | } 141 | ``` 142 | 143 | ```typescript 144 | declare global { 145 | interface Number { 146 | readonly quotientAndRemainderOf: (dividingBy: number) => [number, number]; 147 | readonly isMultipleOf: (number: number) => boolean; 148 | readonly isZero: boolean; 149 | } 150 | 151 | // static properties (e.x. Number.zero) 152 | interface NumberConstructor { 153 | readonly zero: 0; 154 | } 155 | } 156 | ``` 157 | 158 | ```typescript 159 | declare global { 160 | // static properties (e.x. Boolean.random()) 161 | interface BooleanConstructor { 162 | readonly random: () => boolean; 163 | } 164 | } 165 | ``` 166 | 167 | ## Q&A 168 | 169 | **Can I still use `arr[0]`? Do I have to use `arr.first`?** 170 | 171 | TypeSwift doesn't break any existing JS functionality, it just extends it. All existing JS methods and variables will 172 | still work. 173 | 174 | **Why should I install a whole package just to write `arr.last`?** 175 | 176 | If you don't think TypeSwift's extensions would help you, that's cool. I prefer writing 177 | `arr.last` instead of `arr[arr.length - 1]`, but it's all personal preference. 178 | 179 | **I would like X feature from Swift added.** 180 | 181 | Help with features would be greatly appreciated. Please check out existing code to see the style and [open a PR][2]. 182 | 183 | [2]: https://github.com/kyle-n/TypeSwift/pulls 184 | -------------------------------------------------------------------------------- /build/set-version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | SCRIPTPATH="$(cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P)" 4 | cd "$SCRIPTPATH" 5 | cd .. 6 | ./build/update-readme.sh 7 | git add README.md 8 | git commit -m "Updates README" 9 | npm version $1 10 | -------------------------------------------------------------------------------- /build/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": ["ES2020", "dom"], 6 | "declaration": true, 7 | "declarationMap": true, 8 | "sourceMap": true, 9 | "outDir": "../lib", 10 | "rootDir": "../src", 11 | "strict": true, 12 | "noUnusedLocals": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "noImplicitReturns": false, 15 | "allowSyntheticDefaultImports": true, 16 | "esModuleInterop": true, 17 | "forceConsistentCasingInFileNames": true 18 | }, 19 | "include": [ 20 | "../src" 21 | ], 22 | "exclude": [ 23 | "../tests" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /build/update-readme.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | newline=$'\n' 3 | 4 | # Run self in project root, no matter where script is triggered from 5 | SCRIPTPATH="$(cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P)" 6 | cd "$SCRIPTPATH" 7 | cd .. 8 | 9 | # Remove previous content 10 | gsed -i '/^###\sAPI/,/^#/{/^#/!d}' README.md 11 | 12 | # Grab line number of api section 13 | api_line_number=$(grep -n '### API' README.md | cut -d: -f 1) 14 | api_line_number=$((api_line_number+1)) 15 | 16 | code_header='```typescript' 17 | code_footer='```' 18 | gsed -i "${api_line_number}i ${newline}" README.md 19 | 20 | for class in 'Boolean' 'Number' 'String' 'Array' 'Object' 21 | do 22 | # Blank lines around section 23 | gsed -i "${api_line_number}i ${newline}" README.md 24 | next_line=$((api_line_number+1)) 25 | 26 | # file output 27 | file="src/${class}.ts" 28 | content=$(gsed -n '/^export {}/,/^Object\.defineProperties/p;/^Object\.defineProperties/q' $file | gtail -n +2 | ghead -n -2) 29 | formatted_content=$(echo -e "${content}\n" | gsed ':a $!{N; ba}; s/\n/\\n/g') 30 | 31 | # Wrap header declaration and insert into README 32 | insertion=$(gsed -i "${next_line}i ${code_header}${formatted_content}${code_footer}" README.md) 33 | done 34 | 35 | echo "README updated" 36 | -------------------------------------------------------------------------------- /build/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const sharedConfig = { 4 | resolve: { 5 | extensions: ['.js'], 6 | }, 7 | output: { 8 | filename: '[name].js', 9 | path: path.resolve(__dirname, '..', '_bundles'), 10 | }, 11 | }; 12 | 13 | const devConfig = { 14 | ...sharedConfig, 15 | entry: { 16 | 'typeswift': './lib/index.js' 17 | }, 18 | devtool: 'source-map', 19 | mode: 'development' 20 | }; 21 | 22 | const prodConfig = { 23 | ...sharedConfig, 24 | entry: { 25 | 'typeswift.min': './lib/index.js' 26 | }, 27 | mode: 'production' 28 | }; 29 | 30 | module.exports = [devConfig, prodConfig] -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typeswift", 3 | "version": "1.1.7", 4 | "description": "Swift-like extensions for JavaScript and TypeScript", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "type": "commonjs", 8 | "scripts": { 9 | "compile": "tsc -p build/tsconfig.json && webpack -c build/webpack.config.js", 10 | "clean": "shx rm -rf _bundles lib", 11 | "build": "npm run clean && npm run compile", 12 | "prepare": "npm run build", 13 | "preview-npm-files": "npm pack && tar -xvzf *.tgz && rm -rf package *.tgz", 14 | "test": "jest -c tests/jest.config.js" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/kyle-n/TypeSwift.git" 19 | }, 20 | "keywords": [ 21 | "swift", 22 | "typescript", 23 | "javascript" 24 | ], 25 | "author": "Kyle Nazario", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/kyle-n/TypeSwift/issues" 29 | }, 30 | "homepage": "https://github.com/kyle-n/TypeSwift#readme", 31 | "devDependencies": { 32 | "@types/jest": "^26.0.15", 33 | "@types/node": "^14.14.6", 34 | "jest": "^26.6.3", 35 | "shx": "^0.3.3", 36 | "ts-jest": "^26.4.4", 37 | "typescript": "^4.5.2", 38 | "webpack": "^5.64.2", 39 | "webpack-cli": "^4.9.1" 40 | }, 41 | "files": [ 42 | "lib/*", 43 | "_bundles/*", 44 | "package.json" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /src/Array.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | declare global { 4 | interface Array { 5 | readonly first: T | undefined; 6 | readonly last: T | undefined; 7 | readonly isEmpty: boolean; 8 | readonly randomElement: () => T | undefined; 9 | readonly insert: (element: T, at: number) => void; 10 | readonly remove: (at: number) => T | undefined; 11 | readonly compactMap: (callback: (element: T, index?: number, parent?: Array) => any | undefined) => Array; 12 | readonly shuffle: () => void; 13 | readonly shuffled: () => Array; 14 | readonly swapAt: (indexA: number, indexB: number) => void; 15 | readonly startIndex: number | undefined; 16 | readonly endIndex: number | undefined; 17 | } 18 | } 19 | 20 | Object.defineProperties(Array.prototype, { 21 | first: { 22 | get(this: Array) { 23 | if (this.length > 0) return this[0]; 24 | else return undefined; 25 | } 26 | }, 27 | last: { 28 | get(this: Array) { 29 | if (this.length > 0) return this[this.length - 1]; 30 | else return undefined; 31 | } 32 | }, 33 | isEmpty: { 34 | get(this: Array) { 35 | return this.length < 1; 36 | } 37 | }, 38 | randomElement: { 39 | get(this: Array) { 40 | return () => { 41 | if (this.isEmpty) return undefined; 42 | const index = Math.floor(Math.random() * this.length); 43 | return this[index]; 44 | }; 45 | } 46 | }, 47 | insert: { 48 | get(this: Array) { 49 | return (element: any, at: number) => { 50 | this.splice(at, 0, element); 51 | } 52 | } 53 | }, 54 | remove: { 55 | get(this: Array) { 56 | return (at: number) => { 57 | this.splice(at, 1); 58 | } 59 | } 60 | }, 61 | compactMap: { 62 | get(this: Array) { 63 | return (callback: (element: T, index?: number, parent?: Array) => any | undefined): Array => { 64 | const results: Array = []; 65 | for (let i = 0; i < this.length; i++) { 66 | const result = callback(this[i], i, this); 67 | if (result !== undefined && result !== null) results.push(result); 68 | } 69 | return results; 70 | }; 71 | } 72 | }, 73 | shuffled: { 74 | get(this: Array) { 75 | return () => { 76 | const clone = this.slice(); 77 | clone.shuffle(); 78 | return clone; 79 | }; 80 | } 81 | }, 82 | shuffle: { 83 | get(this: Array) { 84 | return () => { 85 | for (let i = this.endIndex ?? -1; i > 0; i--) { 86 | const j = Math.floor(Math.random() * (i + 1)); 87 | let tmp = this[i]; 88 | this[i] = this[j]; 89 | this[j] = tmp; 90 | } 91 | }; 92 | } 93 | }, 94 | swapAt: { 95 | get(this: Array) { 96 | return (indexA: number, indexB: number) => { 97 | const temp = this[indexB]; 98 | this[indexB] = this[indexA]; 99 | this[indexA] = temp; 100 | }; 101 | } 102 | }, 103 | startIndex: { 104 | get(this: Array) { 105 | const indices = Object.keys(this); 106 | if (indices.isEmpty || typeof indices.first !== 'string') return undefined; 107 | const startIndex: number = parseInt(indices.first); 108 | if (isNaN(startIndex)) return undefined; 109 | else return startIndex; 110 | } 111 | }, 112 | endIndex: { 113 | get(this: Array) { 114 | const indices = Object.keys(this); 115 | if (indices.isEmpty || typeof indices.last !== 'string') return undefined; 116 | const endIndex: number = parseInt(indices.last); 117 | if (isNaN(endIndex)) return undefined; 118 | else return endIndex; 119 | } 120 | } 121 | }); 122 | -------------------------------------------------------------------------------- /src/Boolean.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | declare global { 4 | // static properties (e.x. Boolean.random()) 5 | interface BooleanConstructor { 6 | readonly random: () => boolean; 7 | } 8 | } 9 | 10 | Object.defineProperties(Boolean, { 11 | random: { 12 | get(this: Boolean) { 13 | return () => Math.random() > 0.5; 14 | } 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /src/Number.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | declare global { 4 | interface Number { 5 | readonly quotientAndRemainderOf: (dividingBy: number) => [number, number]; 6 | readonly isMultipleOf: (number: number) => boolean; 7 | readonly isZero: boolean; 8 | } 9 | 10 | // static properties (e.x. Number.zero) 11 | interface NumberConstructor { 12 | readonly zero: 0; 13 | } 14 | } 15 | 16 | Object.defineProperties(Number, { 17 | zero: { 18 | get(this: Number) { 19 | return 0; 20 | } 21 | } 22 | }); 23 | 24 | Object.defineProperties(Number.prototype, { 25 | quotientAndRemainderOf: { 26 | get(this: Number) { 27 | return (dividingBy: number) => { 28 | const n: number = this.valueOf(); 29 | const divisionResult = n / dividingBy; 30 | const quotient = Math.floor(divisionResult); 31 | const remainder = n - (quotient * dividingBy); 32 | return [quotient, remainder]; 33 | }; 34 | } 35 | }, 36 | isMultipleOf: { 37 | get(this: Number) { 38 | return (of: number) => { 39 | const n: number = this.valueOf(); 40 | return (n % of === 0); 41 | }; 42 | } 43 | }, 44 | isZero: { 45 | get(this: Number) { 46 | return this.valueOf() === 0; 47 | } 48 | } 49 | }); 50 | -------------------------------------------------------------------------------- /src/Object.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | type UniquingKeysCallback = (valueOne: any, valueTwo: any) => any; 4 | type KeyedObject = {[key: string]: any}; 5 | type KeyValueAssertionCallback = (key: string | number, value: E) => boolean; 6 | 7 | declare global { 8 | interface Object { 9 | readonly isEmpty: boolean; 10 | readonly count: number; 11 | readonly first: any; 12 | readonly randomElement: () => any; 13 | readonly merge: (objectToMerge: KeyedObject, uniquingKeysWith: UniquingKeysCallback) => void; 14 | readonly merging: (objectToMerge: KeyedObject, uniquingKeysWith: UniquingKeysCallback) => KeyedObject; 15 | readonly removeValue: (forKey: string | number) => void; 16 | readonly removeAll: () => void; 17 | readonly contains: (where: KeyValueAssertionCallback) => boolean; 18 | readonly allSatisfy: (callback: KeyValueAssertionCallback) => boolean; 19 | } 20 | } 21 | 22 | Object.defineProperties(Object.prototype, { 23 | isEmpty: { 24 | get(this: Object) { 25 | return Object.keys(this).length === 0; 26 | } 27 | }, 28 | count: { 29 | get(this: Object) { 30 | return Object.keys(this).length; 31 | } 32 | }, 33 | first: { 34 | get(this: Object) { 35 | return Object.values(this)[0]; 36 | } 37 | }, 38 | randomElement: { 39 | get(this: Object) { 40 | return () => { 41 | const index = Math.floor(Math.random() * Object.values(this).length); 42 | return Object.values(this)[index]; 43 | }; 44 | } 45 | }, 46 | merge: { 47 | get(this: KeyedObject) { 48 | return (objectToMerge: KeyedObject, uniquingKeysWith: UniquingKeysCallback): void => { 49 | Object.keys(objectToMerge).forEach(key => { 50 | if (this.hasOwnProperty(key)) this[key] = uniquingKeysWith(this[key], objectToMerge[key]); 51 | else this[key] = objectToMerge[key]; 52 | }); 53 | }; 54 | } 55 | }, 56 | merging: { 57 | get(this: Object) { 58 | return (objectToMerge: KeyedObject, uniquingKeysWith: UniquingKeysCallback): KeyedObject => { 59 | const merged = {...this}; 60 | merged.merge(objectToMerge, uniquingKeysWith); 61 | return merged; 62 | }; 63 | } 64 | }, 65 | removeValue: { 66 | get(this: any) { 67 | return (forKey: string | number): void => { 68 | delete this[forKey]; 69 | }; 70 | } 71 | }, 72 | removeAll: { 73 | get(this: KeyedObject) { 74 | return (): void => { 75 | Object.keys(this).forEach(key => delete this[key]); 76 | }; 77 | } 78 | }, 79 | contains: { 80 | get(this: KeyedObject) { 81 | return (where: KeyValueAssertionCallback): boolean => { 82 | for (const key in this) { 83 | if (where(key, this[key])) return true; 84 | } 85 | return false; 86 | }; 87 | } 88 | }, 89 | allSatisfy: { 90 | get(this: KeyedObject) { 91 | return (callback: KeyValueAssertionCallback): boolean => { 92 | for (const key in this) { 93 | if (!callback(key, this[key])) return false; 94 | } 95 | return true; 96 | }; 97 | } 98 | } 99 | }); 100 | -------------------------------------------------------------------------------- /src/String.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | declare global { 4 | interface String { 5 | readonly isEmpty: boolean; 6 | readonly inserted: (substring: string, at: number) => string; 7 | readonly first: string | undefined; 8 | readonly last: string | undefined; 9 | readonly randomElement: () => string | undefined; 10 | readonly map: (callback: (char: string, index?: number, parent?: string) => string) => string; 11 | readonly compactMap: (callback: (char: string, index?: number, parent?: string) => string | undefined) => string; 12 | readonly forEach: (callback: (char: string, index?: number, parent?: string) => void) => void; 13 | readonly reduce: (callback: (result: R, char: string, index?: number) => R, initialValue: R) => R; 14 | readonly sorted: () => string; 15 | readonly reversed: () => string; 16 | readonly shuffled: () => string; 17 | readonly startIndex: number | undefined; 18 | readonly endIndex: number | undefined; 19 | readonly prefix: (callback: (char: string, index?: number, parent?: string) => boolean) => string; 20 | } 21 | } 22 | 23 | Object.defineProperties(String.prototype, { 24 | isEmpty: { 25 | get(this: String) { 26 | return this.valueOf().length === 0; 27 | } 28 | }, 29 | inserted: { 30 | get(this: String) { 31 | return (substring: string, at: number) => { 32 | return this.slice(0, at) + substring + this.slice(at); 33 | }; 34 | } 35 | }, 36 | first: { 37 | get(this: String) { 38 | if (this.length > 0) return this[0]; 39 | else return undefined; 40 | } 41 | }, 42 | last: { 43 | get(this: String) { 44 | if (this.length > 0) return this[this.length - 1]; 45 | else return undefined; 46 | } 47 | }, 48 | randomElement: { 49 | get(this: String) { 50 | return () => { 51 | if (this.length < 1) return undefined; 52 | const index = Math.floor(Math.random() * this.length); 53 | return this[index]; 54 | }; 55 | } 56 | }, 57 | map: { 58 | get(this: String) { 59 | return (callback: (char: string, index?: number, parent?: string) => string) => { 60 | let mappedString = ''; 61 | for (let i = 0; i < this.length; i++) { 62 | mappedString += callback(this[i], i, this.valueOf()); 63 | } 64 | return mappedString; 65 | }; 66 | } 67 | }, 68 | compactMap: { 69 | get(this: String) { 70 | return (callback: (char: string, index?: number, parent?: string) => string | undefined) => { 71 | let mappedString = ''; 72 | for (let i = 0; i < this.length; i++) { 73 | const mappedChar = callback(this[i], i, this.valueOf()); 74 | if (typeof mappedChar === 'string') mappedString += mappedChar; 75 | } 76 | return mappedString; 77 | }; 78 | } 79 | }, 80 | forEach: { 81 | get(this: String) { 82 | return (callback: (char: string, index?: number, parent?: string) => void) => { 83 | for (let i = 0; i < this.length; i++) { 84 | callback(this[i], i, this.valueOf()); 85 | } 86 | }; 87 | } 88 | }, 89 | reduce: { 90 | get(this: String) { 91 | return (callback: (result: R, char: string, index?: number) => R, initialValue: R): R => { 92 | let val = initialValue; 93 | for (let i = 0; i < this.length; i++) { 94 | val = callback(val, this[i], i); 95 | } 96 | return val; 97 | }; 98 | } 99 | }, 100 | startIndex: { 101 | get(this: String) { 102 | if (this.length > 0) return 0; 103 | else return undefined; 104 | } 105 | }, 106 | endIndex: { 107 | get(this: String) { 108 | if (this.length > 0) return this.length - 1; 109 | else return undefined; 110 | } 111 | }, 112 | sorted: { 113 | get(this: String) { 114 | return () => { 115 | return this.split('').sort().join(''); 116 | }; 117 | } 118 | }, 119 | reversed: { 120 | get(this: String) { 121 | return () => { 122 | let reversed = ''; 123 | for (let i = this.endIndex ?? -1; i >= 0; i--) { 124 | reversed += this[i]; 125 | } 126 | return reversed; 127 | } 128 | } 129 | }, 130 | shuffled: { 131 | get(this: String) { 132 | return () => { 133 | return this.split('').shuffled().join('') 134 | } 135 | } 136 | }, 137 | prefix: { 138 | get(this: String) { 139 | return (callback: (char: string, index?: number, parent?: string) => boolean): string => { 140 | let prefix = ''; 141 | for (let i = 0; i < this.length; i++) { 142 | const charPassesCallback = callback(this[i], i, this.valueOf()); 143 | if (charPassesCallback) prefix += this[i]; 144 | else break; 145 | } 146 | return prefix; 147 | }; 148 | } 149 | } 150 | }); 151 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Array'; 2 | export * from './Number'; 3 | export * from './String'; 4 | export * from './Object'; 5 | export * from './Boolean'; 6 | -------------------------------------------------------------------------------- /tests/Array.test.ts: -------------------------------------------------------------------------------- 1 | import '../src'; 2 | 3 | const getExample = () => [1, 2, undefined, 4]; 4 | 5 | test('Array.first', () => { 6 | expect(getExample().first).toBe(1); 7 | expect([].first).toBe(undefined); 8 | }); 9 | 10 | test('Array.last', () => { 11 | expect(getExample().last).toBe(4); 12 | expect([].last).toBe(undefined); 13 | }); 14 | 15 | test('Array.isEmpty', () => { 16 | expect(getExample().isEmpty).toBe(false); 17 | expect([].isEmpty).toBe(true); 18 | }); 19 | 20 | test('Array.randomElement()', () => { 21 | const num = [1]; 22 | expect(num.randomElement()).toBe(1); 23 | expect([].randomElement()).toBe(undefined); 24 | }); 25 | 26 | test('Array.insert()', () => { 27 | const example = getExample(); 28 | example.insert(99, 0); 29 | expect(example).toEqual([99, 1, 2, undefined, 4]); 30 | }); 31 | 32 | test('Array.remove()', () => { 33 | const example = getExample(); 34 | example.remove(0); 35 | expect(example).toEqual([2, undefined, 4]); 36 | }); 37 | 38 | test('Array.compactMap()', () => { 39 | const nonNullOrUndefinedElements = getExample().compactMap(elem => elem && elem * 2); 40 | expect(nonNullOrUndefinedElements).toEqual([2, 4, 8]); 41 | }); 42 | 43 | test('Array.shuffled()', () => { 44 | const example = getExample(); 45 | const cloned = example.shuffled(); 46 | expect(example).not.toBe(cloned); 47 | }); 48 | 49 | test('Array.swapAt()', () => { 50 | const example = getExample(); 51 | example.swapAt(0, 1); 52 | expect(example).toEqual([2, 1, undefined, 4]); 53 | }); 54 | 55 | test('Array.startIndex', () => { 56 | expect(getExample().startIndex).toBe(0); 57 | expect([].startIndex).toBe(undefined); 58 | }); 59 | 60 | test('Array.endIndex', () => { 61 | expect(getExample().endIndex).toBe(3); 62 | expect([].endIndex).toBe(undefined); 63 | }); 64 | -------------------------------------------------------------------------------- /tests/Boolean.test.ts: -------------------------------------------------------------------------------- 1 | import '../src'; 2 | 3 | test('Boolean.random()', () => { 4 | expect(typeof Boolean.random()).toBe('boolean'); 5 | }); 6 | -------------------------------------------------------------------------------- /tests/Number.test.ts: -------------------------------------------------------------------------------- 1 | import '../src'; 2 | 3 | test('Number.prototype.quotientAndRemainder()', () => { 4 | const n = 42; 5 | expect(n.quotientAndRemainderOf(10)).toEqual([4, 2]); 6 | }); 7 | 8 | test('Number.prototype.isMultipleOf()', () => { 9 | const n = 10; 10 | expect(n.isMultipleOf(2)).toBe(true); 11 | expect(n.isMultipleOf(3)).toBe(false); 12 | }); 13 | 14 | test('Number.prototype.isZero', () => { 15 | const one = 1; 16 | const zero = 0; 17 | expect(one.isZero).toBe(false); 18 | expect(zero.isZero).toBe(true); 19 | }); 20 | 21 | test('Number.zero', () => { 22 | expect(Number.zero).toBe(0); 23 | }); 24 | -------------------------------------------------------------------------------- /tests/Object.test.ts: -------------------------------------------------------------------------------- 1 | import '../src'; 2 | 3 | const getEmpty = () => ({}); 4 | const getObject = () => ({one: 1, two: 2}); 5 | 6 | test('Object.isEmpty', () => { 7 | expect(getEmpty().isEmpty).toBe(true); 8 | expect(getObject().isEmpty).toBe(false); 9 | }); 10 | 11 | test('Object.count', () => { 12 | expect(getEmpty().count).toBe(0); 13 | expect(getObject().count).toBe(2); 14 | }); 15 | 16 | test('Object.first', () => { 17 | expect(getEmpty().first).toBe(undefined); 18 | expect(getObject().first).toBe(1); 19 | }); 20 | 21 | test('Object.randomElement()', () => { 22 | expect(getEmpty().randomElement()).toBe(undefined); 23 | expect(typeof getObject().randomElement()).toBe('number'); 24 | }); 25 | 26 | test('Object.merge()', () => { 27 | const obj = getObject(); 28 | obj.merge({two: 'two', three: 'three'}, (objA, objB) => objB); 29 | expect(obj).toEqual({one: 1, two: 'two', three: 'three'}); 30 | }); 31 | 32 | test('Object.merged()', () => { 33 | const obj = getObject(); 34 | const merged = obj.merging({two: 'two', three: 'three'}, (objA, objB) => objB); 35 | expect(merged).toEqual({one: 1, two: 'two', three: 'three'}); 36 | expect(obj).not.toBe(merged); 37 | }); 38 | 39 | test('Object.removeValue', () => { 40 | const obj = getObject(); 41 | obj.removeValue('one'); 42 | expect(obj).toEqual({two: 2}); 43 | }); 44 | 45 | test('Object.removeAll()', () => { 46 | const obj = getObject(); 47 | obj.removeAll(); 48 | expect(obj).toEqual({}); 49 | }); 50 | 51 | test('Object.contains()', () => { 52 | const obj: any = getObject(); 53 | const hasOne = obj.contains((key: string, val: number) => { 54 | return key === 'one' && val === 1; 55 | }); 56 | const hasFive = obj.contains((key: string, val: number) => { 57 | return key === 'five' && val === 5; 58 | }); 59 | expect(hasOne).toBe(true); 60 | expect(hasFive).toBe(false); 61 | }); 62 | 63 | test('Object.allSatisfy()', () => { 64 | const obj = getObject(); 65 | const allNumbers = obj.allSatisfy((key, val) => { 66 | return typeof val === 'number'; 67 | }); 68 | const allBooleans = obj.allSatisfy((key, val) => { 69 | return typeof val === 'boolean'; 70 | }); 71 | expect(allNumbers).toBe(true); 72 | expect(allBooleans).toBe(false); 73 | }); 74 | -------------------------------------------------------------------------------- /tests/String.test.ts: -------------------------------------------------------------------------------- 1 | import '../src'; 2 | 3 | const getEmpty = () => ''; 4 | const getABC = () => 'abc'; 5 | 6 | test('String.isEmpty', () => { 7 | expect(getEmpty().isEmpty).toBe(true); 8 | expect(getABC().isEmpty).toBe(false); 9 | }); 10 | 11 | test('String.inserted()', () => { 12 | expect(getABC().inserted('x', 0)).toBe('xabc'); 13 | }); 14 | 15 | test('String.first', () => { 16 | expect(getEmpty().first).toBe(undefined); 17 | expect(getABC().first).toBe('a'); 18 | }); 19 | 20 | test('String.last', () => { 21 | expect(getEmpty().last).toBe(undefined); 22 | expect(getABC().last).toBe('c'); 23 | }); 24 | 25 | test('String.randomElement()', () => { 26 | expect(getEmpty().randomElement()).toBe(undefined); 27 | expect(typeof getABC().randomElement()).toBe('string'); 28 | }); 29 | 30 | test('String.map()', () => { 31 | const mapped = getABC().map(() => 'q'); 32 | expect(mapped).toBe('qqq'); 33 | }); 34 | 35 | test('String.compactMap()', () => { 36 | const mapped = getABC().compactMap(letter => { 37 | return letter === 'a' ? 'q' : undefined; 38 | }); 39 | expect(mapped).toBe('q'); 40 | }); 41 | 42 | test('String.forEach()', () => { 43 | let i = 0; 44 | getABC().forEach(() => i++); 45 | expect(i).toBe(getABC().length); 46 | }); 47 | 48 | test('String.reduce()', () => { 49 | const aCount = getABC().reduce((numberOfAs, letter) => { 50 | return letter === 'a' ? numberOfAs + 1 : numberOfAs; 51 | }, 0); 52 | expect(aCount).toBe(1); 53 | }); 54 | 55 | test('String.sorted()', () => { 56 | const shuffled = 'cab'; 57 | expect(shuffled.sorted()).toBe('abc'); 58 | }); 59 | 60 | test('String.reversed()', () => { 61 | expect(getABC().reversed()).toBe('cba'); 62 | }); 63 | 64 | test('String.shuffled() exists', () => { 65 | expect(getABC().shuffled).not.toBe(undefined); 66 | }); 67 | 68 | test('String.startIndex', () => { 69 | expect(getEmpty().startIndex).toBe(undefined); 70 | expect(getABC().startIndex).toBe(0); 71 | }); 72 | 73 | test('String.endIndex', () => { 74 | expect(getEmpty().endIndex).toBe(undefined); 75 | expect(getABC().endIndex).toBe(2); 76 | }); 77 | 78 | test('String.prefix', () => { 79 | const letters = 'aabbcc'; 80 | const leadingAs = letters.prefix(char => char === 'a'); 81 | expect(leadingAs.length).toBe(2); 82 | }); 83 | -------------------------------------------------------------------------------- /tests/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rootDir: "../", 3 | roots: [ 4 | "/src", 5 | "/tests" 6 | ], 7 | testMatch: [ 8 | "**/*.test.ts" 9 | ], 10 | transform: { 11 | "^.+\\.ts$": "ts-jest" 12 | }, 13 | clearMocks: true, 14 | coverageProvider: "v8", 15 | testEnvironment: "node" 16 | }; 17 | --------------------------------------------------------------------------------