├── .eslintrc.js ├── .github └── workflows │ ├── codeql-analysis.yml │ ├── coverage.yml │ └── publish.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── SECURITY.md ├── jest.config.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── index.test.ts └── index.ts └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2020: true, 5 | }, 6 | extends: ['plugin:@typescript-eslint/recommended', 'prettier'], 7 | parser: '@typescript-eslint/parser', 8 | parserOptions: { 9 | ecmaVersion: 11, 10 | sourceType: 'module', 11 | }, 12 | plugins: ['@typescript-eslint', 'prettier'], 13 | rules: { 14 | 'linebreak-style': ['error', 'unix'], 15 | quotes: ['error', 'single'], 16 | semi: ['error', 'always'], 17 | 'import/no-extraneous-dependencies': ['off'], 18 | 'import/prefer-default-export': ['off'], 19 | 'no-plusplus': ['off'], 20 | 'object-curly-newline': ['off'], 21 | 'no-restricted-syntax': ['off'], 22 | 'no-continue': ['off'], 23 | 'no-use-before-define': ['off'], 24 | 'no-return-assign': ['off'], 25 | 'no-param-reassign': ['off'], 26 | 'no-extra-parens': ['error', 'all'], 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '26 21 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: coveralls test coverage 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | release: 9 | types: 10 | - created 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v1 17 | - uses: actions/setup-node@v1 18 | with: 19 | node-version: 12 20 | - run: npm install svelte@^3.44.2 21 | - run: npm ci 22 | - run: npm test 23 | - uses: coverallsapp/github-action@master 24 | with: 25 | github-token: ${{ secrets.GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: publish 5 | 6 | on: 7 | release: 8 | types: [published] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: 12 18 | - run: npm install svelte@^3.49.0 19 | - run: npm ci 20 | - run: npm test 21 | 22 | publish-npm: 23 | needs: build 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v2 27 | - uses: actions/setup-node@v1 28 | with: 29 | node-version: 12 30 | registry-url: https://registry.npmjs.org/ 31 | - run: npm install svelte@^3.49.0 32 | - run: npm ci 33 | - run: npm publish 34 | env: 35 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | coverage 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "printWidth": 80 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Bryan Lee 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 | ![svelte-previous-banner](https://user-images.githubusercontent.com/42545742/102723346-20ac5700-4342-11eb-978d-222a2f4109d5.png) 2 | 3 | # svelte-previous 4 | 5 | [![npm version](http://img.shields.io/npm/v/svelte-previous.svg)](https://www.npmjs.com/package/svelte-previous) 6 | [![npm downloads](https://img.shields.io/npm/dm/svelte-previous.svg)](https://www.npmjs.com/package/svelte-previous) 7 | ![license](https://img.shields.io/npm/l/svelte-previous) 8 | ![build](https://img.shields.io/github/actions/workflow/status/bryanmylee/svelte-previous/publish.yml) 9 | [![coverage](https://coveralls.io/repos/github/bryanmylee/svelte-previous/badge.svg?branch=master)](https://coveralls.io/github/bryanmylee/svelte-previous?branch=master) 10 | [![size](https://img.shields.io/bundlephobia/min/svelte-previous)](https://bundlephobia.com/result?p=svelte-previous) 11 | 12 | Svelte stores that remember previous values! 13 | 14 | This allows us to perform actions that depend on previous values, such as transitions between old and new values. 15 | 16 | ## Installation 17 | 18 | ```bash 19 | $ npm i -D svelte-previous 20 | ``` 21 | 22 | Since Svelte automatically bundles all required dependencies, you only need to install this package as a dev dependency with the -D flag. 23 | 24 | ## Demo 25 | 26 | Visit the [REPL demo](https://svelte.dev/repl/1d3e752c51b848e6af264f3244f3e85c?version=3.31.0). 27 | 28 | ## Usage 29 | 30 | `withPrevious` accepts an initial value, and returns a tuple comprising a [Writable](https://svelte.dev/tutorial/writable-stores) and a [Readable](https://svelte.dev/tutorial/readable-stores) store. 31 | 32 | ```svelte 33 | 42 | 43 | transition from {$previousName} to {$currentName}. 44 | ``` 45 | 46 | ## Options 47 | 48 | `withPrevious` takes an options object as its second argument. 49 | 50 | ### `numToTrack: number` 51 | 52 | By default, `withPrevious` tracks one previous value. 53 | 54 | To track more than one value, set `numToTrack`. 55 | 56 | ```svelte 57 | 60 | 61 | from {$prev2} to {$prev1} to {$current}. 62 | ``` 63 | 64 | ### `initPrevious: T[]` 65 | 66 | To initialize previous values with something besides `null`, pass an array of values from newest to oldest. 67 | 68 | Missing values will be filled with `null` and extra values will be ignored. 69 | 70 | ```svelte 71 | 74 | 75 | from {$prev2} to {$prev1} to {$current}. 76 | ``` 77 | 78 | ### `requireChange: boolean` 79 | 80 | Due to how reactivity is handled in Svelte, some assignments may assign the same value multiple times to a variable. Therefore, to prevent a single value from overwriting all previous values, a change in value is required before the current and previous values are updated. 81 | 82 | Set `requireChange = false` to change this behaviour. 83 | 84 | ```ts 85 | const [current, previous] = withPrevious(0, { requireChange: false }); 86 | ``` 87 | 88 | ### `isEqual: (a: T, b: T) => boolean` 89 | 90 | By default, equality is determined with the `===` operator. However, `===` only checks equality by reference when comparing objects. 91 | 92 | Provide a custom `isEqual` function to compare objects. 93 | 94 | ```ts 95 | const [current, previous] = withPrevious(0, { 96 | isEqual: (a, b) => a.name === b.name && a.age === b.age, 97 | }); 98 | ``` 99 | 100 | It is also possible to use [lodash.isequal](https://www.npmjs.com/package/lodash.isequal). 101 | 102 | ```ts 103 | import isEqual from 'lodash.isequal'; 104 | 105 | const [current, previous] = withPrevious(0, { 106 | isEqual: isEqual, 107 | }); 108 | ``` 109 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | All versions of `svelte-previous` will be supported for the foreseeable future. 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 2.x.x | :white_check_mark: | 10 | 11 | ## Reporting a Vulnerability 12 | 13 | Send a report to [bryanmylee@gmail.com](mailto:bryanmylee@gmail.com?subject=svelte-previous%20Vulnerability). 14 | 15 | Vulnerabilities will be addressed within one working week, and a minor-version will be released if possible. 16 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['./src'], 3 | testMatch: ['**/__tests__/**/*.+(ts|js)', '**/?(*.)+(spec|test).+(ts|js)'], 4 | transform: { 5 | '^.+\\.(ts)$': 'ts-jest', 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-previous", 3 | "version": "2.1.4", 4 | "description": "Svelte stores that remember previous values", 5 | "main": "dist/index.js", 6 | "module": "dist/index.es.js", 7 | "types": "dist/index.d.ts", 8 | "files": [ 9 | "dist" 10 | ], 11 | "scripts": { 12 | "prepare": "rollup -c && tsc", 13 | "pretest": "eslint --ignore-path .gitignore .", 14 | "test": "jest --collect-coverage", 15 | "format": "prettier --plugin-search-dir=. --write --ignore-path .gitignore ." 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+ssh://git@github.com/bryanmylee/svelte-previous.git" 20 | }, 21 | "keywords": [ 22 | "svelte", 23 | "sveltejs", 24 | "store", 25 | "previous", 26 | "memo" 27 | ], 28 | "author": "Bryan Lee", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/bryanmylee/svelte-previous/issues" 32 | }, 33 | "homepage": "https://github.com/bryanmylee/svelte-previous#readme", 34 | "peerDependencies": { 35 | "svelte": "^3.49.0 || ^4" 36 | }, 37 | "devDependencies": { 38 | "@rollup/plugin-typescript": "^8.1.0", 39 | "@types/jest": "^26.0.19", 40 | "@typescript-eslint/eslint-plugin": "^4.11.0", 41 | "@typescript-eslint/parser": "^4.11.0", 42 | "eslint": "^7.16.0", 43 | "eslint-config-prettier": "^8.3.0", 44 | "eslint-plugin-import": "^2.22.1", 45 | "eslint-plugin-prettier": "^4.0.0", 46 | "jest": "^26.6.3", 47 | "prettier": "^2.5.1", 48 | "rollup": "^2.35.1", 49 | "ts-jest": "^26.4.4", 50 | "tslib": "^2.0.3", 51 | "typescript": "^4.5.2" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import pkg from './package.json'; 3 | import tsconfig from './tsconfig.json'; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 6 | const { declaration, declarationDir, ...compilerOptions } = 7 | tsconfig.compilerOptions; 8 | 9 | export default { 10 | external: ['svelte'], 11 | input: 'src/index.ts', 12 | output: [ 13 | { format: 'cjs', file: pkg.main }, 14 | { format: 'es', file: pkg.module }, 15 | ], 16 | plugins: [ 17 | typescript({ 18 | tsconfig: false, 19 | ...compilerOptions, 20 | }), 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { get } from 'svelte/store'; 2 | import { withPrevious } from './index'; 3 | 4 | describe('initialization', () => { 5 | it('initializes the current and previous null value', () => { 6 | // Arrange 7 | const [current, previous] = withPrevious(0); 8 | 9 | // Assert 10 | expect(get(current)).toBe(0); 11 | expect(get(previous)).toBeNull(); 12 | }); 13 | 14 | it('initializes the current and two previous null values', () => { 15 | // Arrange 16 | const [current, prev1, prev2] = withPrevious(0, { numToTrack: 2 }); 17 | 18 | // Assert 19 | expect(get(current)).toBe(0); 20 | expect(get(prev1)).toBeNull(); 21 | expect(get(prev2)).toBeNull(); 22 | }); 23 | 24 | it('throws an error if no values are tracked', () => { 25 | expect(withPrevious.bind(this, 0, { numToTrack: 0 })).toThrow( 26 | 'Must track at least 1 previous' 27 | ); 28 | }); 29 | describe('with initial previous values', () => { 30 | it('initializes multiple previous values', () => { 31 | // Arrange 32 | const [current, prev1, prev2] = withPrevious(0, { 33 | numToTrack: 2, 34 | initPrevious: [1, 2], 35 | }); 36 | 37 | // Assert 38 | expect(get(current)).toBe(0); 39 | expect(get(prev1)).toBe(1); 40 | expect(get(prev2)).toBe(2); 41 | }); 42 | 43 | it('initializes fewer previous values than tracked', () => { 44 | // Arrange 45 | const [current, prev1, prev2] = withPrevious(0, { 46 | numToTrack: 2, 47 | initPrevious: [1], 48 | }); 49 | 50 | // Assert 51 | expect(get(current)).toBe(0); 52 | expect(get(prev1)).toBe(1); 53 | expect(get(prev2)).toBeNull(); 54 | }); 55 | 56 | it('initializes more previous values than tracked', () => { 57 | // Arrange 58 | const [current, prev1, prev2] = withPrevious(0, { 59 | numToTrack: 2, 60 | initPrevious: [1, 2, 3, 4], 61 | }); 62 | 63 | // Assert 64 | expect(get(current)).toBe(0); 65 | expect(get(prev1)).toBe(1); 66 | expect(get(prev2)).toBe(2); 67 | }); 68 | }); 69 | }); 70 | 71 | describe('update previous', () => { 72 | it('sets one previous value', () => { 73 | // Arrange 74 | const [current, previous] = withPrevious(0); 75 | 76 | // Act and Assert 77 | current.set(1); 78 | expect(get(current)).toBe(1); 79 | expect(get(previous)).toBe(0); 80 | 81 | current.set(2); 82 | expect(get(current)).toBe(2); 83 | expect(get(previous)).toBe(1); 84 | }); 85 | 86 | it('sets two previous values', () => { 87 | // Arrange 88 | const [current, prev1, prev2] = withPrevious(0, { numToTrack: 2 }); 89 | 90 | // Act and Assert 91 | current.set(1); 92 | expect(get(current)).toBe(1); 93 | expect(get(prev1)).toBe(0); 94 | expect(get(prev2)).toBeNull(); 95 | 96 | current.set(2); 97 | expect(get(current)).toBe(2); 98 | expect(get(prev1)).toBe(1); 99 | expect(get(prev2)).toBe(0); 100 | 101 | current.set(3); 102 | expect(get(current)).toBe(3); 103 | expect(get(prev1)).toBe(2); 104 | expect(get(prev2)).toBe(1); 105 | }); 106 | }); 107 | 108 | describe('equality', () => { 109 | it('updates even if the values are equal', () => { 110 | // Arrange 111 | const [current, previous] = withPrevious(0, { requireChange: false }); 112 | 113 | // Act and Assert 114 | current.set(1); 115 | current.set(1); 116 | expect(get(current)).toBe(1); 117 | expect(get(previous)).toBe(1); 118 | }); 119 | 120 | it('updates multiple previous values even if the values are equal', () => { 121 | // Arrange 122 | const [current, prev1, prev2] = withPrevious(0, { 123 | numToTrack: 2, 124 | requireChange: false, 125 | }); 126 | 127 | // Act and Assert 128 | current.set(0); 129 | current.set(0); 130 | expect(get(current)).toBe(0); 131 | expect(get(prev1)).toBe(0); 132 | expect(get(prev2)).toBe(0); 133 | }); 134 | 135 | it('does not update if the values are equal', () => { 136 | // Arrange 137 | const [current, previous] = withPrevious(0); 138 | 139 | // Act and Assert 140 | current.set(1); 141 | current.set(1); 142 | expect(get(current)).toBe(1); 143 | expect(get(previous)).toBe(0); 144 | }); 145 | 146 | it('does not update if object values are equal based on `isEqual`', () => { 147 | // Arrange 148 | const first = { name: 'sam', age: 12 }; 149 | const second = { name: 'john', age: 13 }; 150 | const secondCopy = { name: 'john', age: 13 }; 151 | const [current, previous] = withPrevious(first, { 152 | isEqual: (a, b) => a.name === b.name && a.age === b.age, 153 | }); 154 | 155 | // Act and Assert 156 | current.set(second); 157 | current.set(secondCopy); 158 | expect(get(current)).toBe(second); 159 | expect(get(previous)).toBe(first); 160 | }); 161 | 162 | it('updates if object values are different based on `isEqual`', () => { 163 | // Arrange 164 | const first = { name: 'sam', age: 12 }; 165 | const [current, previous] = withPrevious(first, { 166 | isEqual: (a, b) => a.name === b.name && a.age === b.age, 167 | }); 168 | 169 | // Act 170 | current.update(($current) => ({ ...$current, age: 13 })); 171 | 172 | // Assert 173 | expect(get(current)).toEqual({ name: 'sam', age: 13 }); 174 | expect(get(previous)).toEqual({ name: 'sam', age: 12 }); 175 | }); 176 | }); 177 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { writable, derived } from 'svelte/store'; 2 | import type { Writable, Readable, Updater } from 'svelte/store'; 3 | 4 | type WithPreviousOptions = { 5 | numToTrack?: N; 6 | initPrevious?: Array; 7 | requireChange?: boolean; 8 | isEqual?: IsEqual; 9 | }; 10 | 11 | type WithPreviousResult = [ 12 | Writable, 13 | ...Tuple, N> 14 | ]; 15 | 16 | export function withPrevious( 17 | initValue: T, 18 | { 19 | numToTrack = 1 as N, 20 | initPrevious = [], 21 | requireChange = true, 22 | isEqual = (a, b) => a === b, 23 | }: WithPreviousOptions = {} 24 | ): WithPreviousResult { 25 | if (numToTrack <= 0) { 26 | throw new Error('Must track at least 1 previous'); 27 | } 28 | 29 | // Generates an array of size numToTrack with the first element set to 30 | // initValue and all other elements set to ...initPrevious or null. 31 | const rest: Array = initPrevious.slice(0, numToTrack); 32 | while (rest.length < numToTrack) { 33 | rest.push(null); 34 | } 35 | 36 | const values = writable>([initValue, ...rest]); 37 | const updateCurrent = (fn: Updater) => { 38 | values.update(($values) => { 39 | const newValue = fn($values[0]); 40 | // Prevent updates if values are equal as defined by an isEqual 41 | // comparison. By default, use a simple === comparison. 42 | if (requireChange && isEqual(newValue, $values[0])) { 43 | return $values; 44 | } 45 | // Adds the new value to the front of the array and removes the oldest 46 | // value from the end. 47 | return [newValue, ...$values.slice(0, numToTrack)]; 48 | }); 49 | }; 50 | const current = { 51 | subscribe: derived(values, ($values) => $values[0]).subscribe, 52 | update: updateCurrent, 53 | set: (newValue: T) => { 54 | updateCurrent(() => newValue); 55 | }, 56 | }; 57 | // Create an array of derived stores for every other element in the array. 58 | const others = [...Array(numToTrack)].map((_, i) => 59 | derived(values, ($values) => $values[i + 1]) 60 | ); 61 | return [current, ...others] as WithPreviousResult; 62 | } 63 | 64 | /** 65 | * @deprecated Since version 2.0.1. Use `withPrevious` instead. 66 | */ 67 | export const usePrevious = withPrevious; 68 | 69 | // UTILITY TYPES 70 | // ============= 71 | type IsEqual = (a: T, b: T) => boolean; 72 | type NonNullFirstArray = [T, ...Array]; 73 | 74 | /** 75 | * Adopted from https://github.com/microsoft/TypeScript/issues/26223#issuecomment-674514787 76 | */ 77 | type BuildPowersOf2LengthArrays< 78 | N extends number, 79 | R extends never[][] 80 | > = R[0][N] extends never 81 | ? R 82 | : BuildPowersOf2LengthArrays; 83 | 84 | type ConcatLargestUntilDone< 85 | N extends number, 86 | R extends never[][], 87 | B extends never[] 88 | > = B['length'] extends N 89 | ? B 90 | : [...R[0], ...B][N] extends never 91 | ? ConcatLargestUntilDone< 92 | N, 93 | R extends [R[0], ...infer U] ? (U extends never[][] ? U : never) : never, 94 | B 95 | > 96 | : ConcatLargestUntilDone< 97 | N, 98 | R extends [R[0], ...infer U] ? (U extends never[][] ? U : never) : never, 99 | [...R[0], ...B] 100 | >; 101 | 102 | type Replace = { [K in keyof R]: T }; 103 | 104 | type Tuple = number extends N 105 | ? T[] 106 | : { 107 | [K in N]: BuildPowersOf2LengthArrays extends infer U 108 | ? U extends never[][] 109 | ? Replace, T> 110 | : never 111 | : never; 112 | }[N]; 113 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "moduleResolution": "node", 5 | "emitDeclarationOnly": true, 6 | "declaration": true, 7 | "declarationDir": "dist", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true 11 | }, 12 | "exclude": ["node_modules", "dist", "**/*.test.ts"] 13 | } 14 | --------------------------------------------------------------------------------