├── .gitignore ├── .tool-versions ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── package.json ├── pnpm-lock.yaml ├── src ├── index.ts ├── parsers.spec.ts ├── parsers.ts ├── type-formdata.spec.ts └── typed-formdata.ts ├── tsconfig.json ├── tsconfig.lib.json ├── tsconfig.test.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | tsconfig.lib.tsbuildinfo 4 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 22.6.0 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Tomasz Kielar 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 | # typed-formdata 2 | 3 | `typed-formdata` is a utility library for working with FormData in Typescript. 4 | 5 | In a nutshell, Typed FormData allows you to: 6 | 7 | - Work with FormData with strongly typed fields 8 | - Helpful for both Frontend and Backend work with FormData 9 | - Integrate it with Full stack typescript frameworks like Remix, Next.js, Nest.js 10 | - It is a drop-in replacement for FormData 11 | - It is built on top of the native FormData interface 12 | - _Parse the formData body according to schema (WIP)_ 13 | 14 | ## Installation 15 | 16 | ```sh 17 | npm install @k1eu/typed-formdata 18 | ``` 19 | 20 | ```sh 21 | yarn add @k1eu/typed-formdata 22 | ``` 23 | 24 | ```sh 25 | pnpm add @k1eu/typed-formdata 26 | ``` 27 | 28 | ```sh 29 | bun add @k1eu/typed-formdata 30 | ``` 31 | 32 | ## Overview 33 | 34 | Package can help you both on Frontend and Backend side of the application. 35 | It provides a `TypedFormData` class and parser functions for Request and FormData. Of course it is advised to have a validation layer in your backend until we have a schema validator implemented in the library. 36 | 37 | Request handler: 38 | 39 | ```ts 40 | import { TypedFormData } from "@k1eu/typed-formdata"; 41 | 42 | type IncomingData = { 43 | resourceId: string; 44 | file: File; 45 | }; 46 | 47 | export const handler = async (req: Request) => { 48 | const formData = parseFormDataRequest(req); 49 | const resourceId: string = formData.get("resourceId"); 50 | const file: File = formData.get("file"); 51 | const age: string = formData.get("age"); // Type Error! Age doesn't exist in IncomingData 52 | 53 | saveFile(file, resourceId); 54 | 55 | return new Response( 56 | `Hello your file ${file.name} is saved for the resource ${resourceId}` 57 | ); 58 | }; 59 | ``` 60 | 61 | Frontend form: 62 | 63 | ```ts 64 | import { TypedFormData } from "@k1eu/typed-formdata"; 65 | 66 | type MyFormData = { 67 | login: string; 68 | password: string; 69 | }; 70 | 71 | function MyPage() { 72 | return ( 73 |
{ 75 | e.preventDefault(); 76 | const formData = new TypedFormData(e.currentTarget); 77 | const login: string = formData.get("login"); 78 | const password: string = formData.get("password"); 79 | loginAndSubmit(login, password); 80 | }} 81 | > 82 | 83 | 84 | 85 | 86 | ); 87 | } 88 | ``` 89 | 90 | Remix action: 91 | 92 | ```tsx 93 | import { TypedFormData } from "@k1eu/typed-formdata"; 94 | 95 | type FormFields = { 96 | login: string; 97 | password: string; 98 | }; 99 | 100 | export async function action({ request }: ActionArgs) { 101 | const formData = parseFormDataRequest(request); 102 | const login: string = formData.get("login"); 103 | const password: string = formData.get("password"); 104 | const file = formData.get("file"); // Type Error! 105 | loginAndSubmit(login, password); 106 | return redirect("/success"); 107 | } 108 | 109 | export default function MyPage() { 110 | return ( 111 |
112 |
113 | 114 | 115 | 116 |
117 |
118 | ); 119 | } 120 | ``` 121 | 122 | Other Parser functions: 123 | 124 | ```ts 125 | // parseFormData 126 | import { parseFormData } from "@k1eu/typed-formdata"; 127 | 128 | type FormFields = { 129 | resourceId: string; 130 | file: File; 131 | }; 132 | 133 | const formData = new FormData(document.querySelector("form")); 134 | const typedFormData = await parseFormData(formData); 135 | 136 | // same as 137 | // const typedFormData = new TypedFormData(document.querySelector("form") as HTMLFormElement); 138 | ``` 139 | 140 | ## License 141 | 142 | See [LICENSE](https://github.com/k1eu/typed-formdata/blob/main/LICENSE) 143 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@k1eu/typed-formdata", 3 | "version": "0.9.1", 4 | "description": "A typed version of FormData", 5 | "scripts": { 6 | "test": "tsc --p ./tsconfig.test.json && vitest run --typecheck --dom", 7 | "test:watch": "vitest --typecheck", 8 | "build": "tsc --outDir dist --project tsconfig.lib.json" 9 | }, 10 | "keywords": [ 11 | "FormData", 12 | "forms", 13 | "form", 14 | "request", 15 | "typed" 16 | ], 17 | "author": "Tomasz Kielar ", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/k1eu/typed-formdata.git" 21 | }, 22 | "files": [ 23 | "dist", 24 | "src", 25 | "LICENSE", 26 | "README.md" 27 | ], 28 | "type": "module", 29 | "exports": { 30 | ".": "./dist/index.js", 31 | "./package.json": "./package.json" 32 | }, 33 | "license": "MIT", 34 | "packageManager": "pnpm@9.4.0", 35 | "devDependencies": { 36 | "@types/node": "^22.2.0", 37 | "happy-dom": "^14.12.3", 38 | "typescript": "^5.5.4", 39 | "vite": "^5.4.0", 40 | "vitest": "^2.0.5" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | devDependencies: 11 | '@types/node': 12 | specifier: ^22.2.0 13 | version: 22.2.0 14 | happy-dom: 15 | specifier: ^14.12.3 16 | version: 14.12.3 17 | typescript: 18 | specifier: ^5.5.4 19 | version: 5.5.4 20 | vite: 21 | specifier: ^5.4.0 22 | version: 5.4.0(@types/node@22.2.0) 23 | vitest: 24 | specifier: ^2.0.5 25 | version: 2.0.5(@types/node@22.2.0)(happy-dom@14.12.3) 26 | 27 | packages: 28 | 29 | '@ampproject/remapping@2.3.0': 30 | resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} 31 | engines: {node: '>=6.0.0'} 32 | 33 | '@esbuild/aix-ppc64@0.21.5': 34 | resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} 35 | engines: {node: '>=12'} 36 | cpu: [ppc64] 37 | os: [aix] 38 | 39 | '@esbuild/android-arm64@0.21.5': 40 | resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} 41 | engines: {node: '>=12'} 42 | cpu: [arm64] 43 | os: [android] 44 | 45 | '@esbuild/android-arm@0.21.5': 46 | resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} 47 | engines: {node: '>=12'} 48 | cpu: [arm] 49 | os: [android] 50 | 51 | '@esbuild/android-x64@0.21.5': 52 | resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} 53 | engines: {node: '>=12'} 54 | cpu: [x64] 55 | os: [android] 56 | 57 | '@esbuild/darwin-arm64@0.21.5': 58 | resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} 59 | engines: {node: '>=12'} 60 | cpu: [arm64] 61 | os: [darwin] 62 | 63 | '@esbuild/darwin-x64@0.21.5': 64 | resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} 65 | engines: {node: '>=12'} 66 | cpu: [x64] 67 | os: [darwin] 68 | 69 | '@esbuild/freebsd-arm64@0.21.5': 70 | resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} 71 | engines: {node: '>=12'} 72 | cpu: [arm64] 73 | os: [freebsd] 74 | 75 | '@esbuild/freebsd-x64@0.21.5': 76 | resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} 77 | engines: {node: '>=12'} 78 | cpu: [x64] 79 | os: [freebsd] 80 | 81 | '@esbuild/linux-arm64@0.21.5': 82 | resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} 83 | engines: {node: '>=12'} 84 | cpu: [arm64] 85 | os: [linux] 86 | 87 | '@esbuild/linux-arm@0.21.5': 88 | resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} 89 | engines: {node: '>=12'} 90 | cpu: [arm] 91 | os: [linux] 92 | 93 | '@esbuild/linux-ia32@0.21.5': 94 | resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} 95 | engines: {node: '>=12'} 96 | cpu: [ia32] 97 | os: [linux] 98 | 99 | '@esbuild/linux-loong64@0.21.5': 100 | resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} 101 | engines: {node: '>=12'} 102 | cpu: [loong64] 103 | os: [linux] 104 | 105 | '@esbuild/linux-mips64el@0.21.5': 106 | resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} 107 | engines: {node: '>=12'} 108 | cpu: [mips64el] 109 | os: [linux] 110 | 111 | '@esbuild/linux-ppc64@0.21.5': 112 | resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} 113 | engines: {node: '>=12'} 114 | cpu: [ppc64] 115 | os: [linux] 116 | 117 | '@esbuild/linux-riscv64@0.21.5': 118 | resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} 119 | engines: {node: '>=12'} 120 | cpu: [riscv64] 121 | os: [linux] 122 | 123 | '@esbuild/linux-s390x@0.21.5': 124 | resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} 125 | engines: {node: '>=12'} 126 | cpu: [s390x] 127 | os: [linux] 128 | 129 | '@esbuild/linux-x64@0.21.5': 130 | resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} 131 | engines: {node: '>=12'} 132 | cpu: [x64] 133 | os: [linux] 134 | 135 | '@esbuild/netbsd-x64@0.21.5': 136 | resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} 137 | engines: {node: '>=12'} 138 | cpu: [x64] 139 | os: [netbsd] 140 | 141 | '@esbuild/openbsd-x64@0.21.5': 142 | resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} 143 | engines: {node: '>=12'} 144 | cpu: [x64] 145 | os: [openbsd] 146 | 147 | '@esbuild/sunos-x64@0.21.5': 148 | resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} 149 | engines: {node: '>=12'} 150 | cpu: [x64] 151 | os: [sunos] 152 | 153 | '@esbuild/win32-arm64@0.21.5': 154 | resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} 155 | engines: {node: '>=12'} 156 | cpu: [arm64] 157 | os: [win32] 158 | 159 | '@esbuild/win32-ia32@0.21.5': 160 | resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} 161 | engines: {node: '>=12'} 162 | cpu: [ia32] 163 | os: [win32] 164 | 165 | '@esbuild/win32-x64@0.21.5': 166 | resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} 167 | engines: {node: '>=12'} 168 | cpu: [x64] 169 | os: [win32] 170 | 171 | '@jridgewell/gen-mapping@0.3.5': 172 | resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} 173 | engines: {node: '>=6.0.0'} 174 | 175 | '@jridgewell/resolve-uri@3.1.2': 176 | resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 177 | engines: {node: '>=6.0.0'} 178 | 179 | '@jridgewell/set-array@1.2.1': 180 | resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} 181 | engines: {node: '>=6.0.0'} 182 | 183 | '@jridgewell/sourcemap-codec@1.5.0': 184 | resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} 185 | 186 | '@jridgewell/trace-mapping@0.3.25': 187 | resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} 188 | 189 | '@rollup/rollup-android-arm-eabi@4.20.0': 190 | resolution: {integrity: sha512-TSpWzflCc4VGAUJZlPpgAJE1+V60MePDQnBd7PPkpuEmOy8i87aL6tinFGKBFKuEDikYpig72QzdT3QPYIi+oA==} 191 | cpu: [arm] 192 | os: [android] 193 | 194 | '@rollup/rollup-android-arm64@4.20.0': 195 | resolution: {integrity: sha512-u00Ro/nok7oGzVuh/FMYfNoGqxU5CPWz1mxV85S2w9LxHR8OoMQBuSk+3BKVIDYgkpeOET5yXkx90OYFc+ytpQ==} 196 | cpu: [arm64] 197 | os: [android] 198 | 199 | '@rollup/rollup-darwin-arm64@4.20.0': 200 | resolution: {integrity: sha512-uFVfvzvsdGtlSLuL0ZlvPJvl6ZmrH4CBwLGEFPe7hUmf7htGAN+aXo43R/V6LATyxlKVC/m6UsLb7jbG+LG39Q==} 201 | cpu: [arm64] 202 | os: [darwin] 203 | 204 | '@rollup/rollup-darwin-x64@4.20.0': 205 | resolution: {integrity: sha512-xbrMDdlev53vNXexEa6l0LffojxhqDTBeL+VUxuuIXys4x6xyvbKq5XqTXBCEUA8ty8iEJblHvFaWRJTk/icAQ==} 206 | cpu: [x64] 207 | os: [darwin] 208 | 209 | '@rollup/rollup-linux-arm-gnueabihf@4.20.0': 210 | resolution: {integrity: sha512-jMYvxZwGmoHFBTbr12Xc6wOdc2xA5tF5F2q6t7Rcfab68TT0n+r7dgawD4qhPEvasDsVpQi+MgDzj2faOLsZjA==} 211 | cpu: [arm] 212 | os: [linux] 213 | 214 | '@rollup/rollup-linux-arm-musleabihf@4.20.0': 215 | resolution: {integrity: sha512-1asSTl4HKuIHIB1GcdFHNNZhxAYEdqML/MW4QmPS4G0ivbEcBr1JKlFLKsIRqjSwOBkdItn3/ZDlyvZ/N6KPlw==} 216 | cpu: [arm] 217 | os: [linux] 218 | 219 | '@rollup/rollup-linux-arm64-gnu@4.20.0': 220 | resolution: {integrity: sha512-COBb8Bkx56KldOYJfMf6wKeYJrtJ9vEgBRAOkfw6Ens0tnmzPqvlpjZiLgkhg6cA3DGzCmLmmd319pmHvKWWlQ==} 221 | cpu: [arm64] 222 | os: [linux] 223 | 224 | '@rollup/rollup-linux-arm64-musl@4.20.0': 225 | resolution: {integrity: sha512-+it+mBSyMslVQa8wSPvBx53fYuZK/oLTu5RJoXogjk6x7Q7sz1GNRsXWjn6SwyJm8E/oMjNVwPhmNdIjwP135Q==} 226 | cpu: [arm64] 227 | os: [linux] 228 | 229 | '@rollup/rollup-linux-powerpc64le-gnu@4.20.0': 230 | resolution: {integrity: sha512-yAMvqhPfGKsAxHN8I4+jE0CpLWD8cv4z7CK7BMmhjDuz606Q2tFKkWRY8bHR9JQXYcoLfopo5TTqzxgPUjUMfw==} 231 | cpu: [ppc64] 232 | os: [linux] 233 | 234 | '@rollup/rollup-linux-riscv64-gnu@4.20.0': 235 | resolution: {integrity: sha512-qmuxFpfmi/2SUkAw95TtNq/w/I7Gpjurx609OOOV7U4vhvUhBcftcmXwl3rqAek+ADBwSjIC4IVNLiszoj3dPA==} 236 | cpu: [riscv64] 237 | os: [linux] 238 | 239 | '@rollup/rollup-linux-s390x-gnu@4.20.0': 240 | resolution: {integrity: sha512-I0BtGXddHSHjV1mqTNkgUZLnS3WtsqebAXv11D5BZE/gfw5KoyXSAXVqyJximQXNvNzUo4GKlCK/dIwXlz+jlg==} 241 | cpu: [s390x] 242 | os: [linux] 243 | 244 | '@rollup/rollup-linux-x64-gnu@4.20.0': 245 | resolution: {integrity: sha512-y+eoL2I3iphUg9tN9GB6ku1FA8kOfmF4oUEWhztDJ4KXJy1agk/9+pejOuZkNFhRwHAOxMsBPLbXPd6mJiCwew==} 246 | cpu: [x64] 247 | os: [linux] 248 | 249 | '@rollup/rollup-linux-x64-musl@4.20.0': 250 | resolution: {integrity: sha512-hM3nhW40kBNYUkZb/r9k2FKK+/MnKglX7UYd4ZUy5DJs8/sMsIbqWK2piZtVGE3kcXVNj3B2IrUYROJMMCikNg==} 251 | cpu: [x64] 252 | os: [linux] 253 | 254 | '@rollup/rollup-win32-arm64-msvc@4.20.0': 255 | resolution: {integrity: sha512-psegMvP+Ik/Bg7QRJbv8w8PAytPA7Uo8fpFjXyCRHWm6Nt42L+JtoqH8eDQ5hRP7/XW2UiIriy1Z46jf0Oa1kA==} 256 | cpu: [arm64] 257 | os: [win32] 258 | 259 | '@rollup/rollup-win32-ia32-msvc@4.20.0': 260 | resolution: {integrity: sha512-GabekH3w4lgAJpVxkk7hUzUf2hICSQO0a/BLFA11/RMxQT92MabKAqyubzDZmMOC/hcJNlc+rrypzNzYl4Dx7A==} 261 | cpu: [ia32] 262 | os: [win32] 263 | 264 | '@rollup/rollup-win32-x64-msvc@4.20.0': 265 | resolution: {integrity: sha512-aJ1EJSuTdGnM6qbVC4B5DSmozPTqIag9fSzXRNNo+humQLG89XpPgdt16Ia56ORD7s+H8Pmyx44uczDQ0yDzpg==} 266 | cpu: [x64] 267 | os: [win32] 268 | 269 | '@types/estree@1.0.5': 270 | resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} 271 | 272 | '@types/node@22.2.0': 273 | resolution: {integrity: sha512-bm6EG6/pCpkxDf/0gDNDdtDILMOHgaQBVOJGdwsqClnxA3xL6jtMv76rLBc006RVMWbmaf0xbmom4Z/5o2nRkQ==} 274 | 275 | '@vitest/expect@2.0.5': 276 | resolution: {integrity: sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==} 277 | 278 | '@vitest/pretty-format@2.0.5': 279 | resolution: {integrity: sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==} 280 | 281 | '@vitest/runner@2.0.5': 282 | resolution: {integrity: sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==} 283 | 284 | '@vitest/snapshot@2.0.5': 285 | resolution: {integrity: sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==} 286 | 287 | '@vitest/spy@2.0.5': 288 | resolution: {integrity: sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==} 289 | 290 | '@vitest/utils@2.0.5': 291 | resolution: {integrity: sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==} 292 | 293 | assertion-error@2.0.1: 294 | resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} 295 | engines: {node: '>=12'} 296 | 297 | cac@6.7.14: 298 | resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} 299 | engines: {node: '>=8'} 300 | 301 | chai@5.1.1: 302 | resolution: {integrity: sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==} 303 | engines: {node: '>=12'} 304 | 305 | check-error@2.1.1: 306 | resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} 307 | engines: {node: '>= 16'} 308 | 309 | cross-spawn@7.0.3: 310 | resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} 311 | engines: {node: '>= 8'} 312 | 313 | debug@4.3.6: 314 | resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==} 315 | engines: {node: '>=6.0'} 316 | peerDependencies: 317 | supports-color: '*' 318 | peerDependenciesMeta: 319 | supports-color: 320 | optional: true 321 | 322 | deep-eql@5.0.2: 323 | resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} 324 | engines: {node: '>=6'} 325 | 326 | entities@4.5.0: 327 | resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} 328 | engines: {node: '>=0.12'} 329 | 330 | esbuild@0.21.5: 331 | resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} 332 | engines: {node: '>=12'} 333 | hasBin: true 334 | 335 | estree-walker@3.0.3: 336 | resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} 337 | 338 | execa@8.0.1: 339 | resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} 340 | engines: {node: '>=16.17'} 341 | 342 | fsevents@2.3.3: 343 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 344 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 345 | os: [darwin] 346 | 347 | get-func-name@2.0.2: 348 | resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} 349 | 350 | get-stream@8.0.1: 351 | resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} 352 | engines: {node: '>=16'} 353 | 354 | happy-dom@14.12.3: 355 | resolution: {integrity: sha512-vsYlEs3E9gLwA1Hp+w3qzu+RUDFf4VTT8cyKqVICoZ2k7WM++Qyd2LwzyTi5bqMJFiIC/vNpTDYuxdreENRK/g==} 356 | engines: {node: '>=16.0.0'} 357 | 358 | human-signals@5.0.0: 359 | resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} 360 | engines: {node: '>=16.17.0'} 361 | 362 | is-stream@3.0.0: 363 | resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} 364 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 365 | 366 | isexe@2.0.0: 367 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 368 | 369 | loupe@3.1.1: 370 | resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==} 371 | 372 | magic-string@0.30.11: 373 | resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} 374 | 375 | merge-stream@2.0.0: 376 | resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} 377 | 378 | mimic-fn@4.0.0: 379 | resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} 380 | engines: {node: '>=12'} 381 | 382 | ms@2.1.2: 383 | resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} 384 | 385 | nanoid@3.3.7: 386 | resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} 387 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 388 | hasBin: true 389 | 390 | npm-run-path@5.3.0: 391 | resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} 392 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 393 | 394 | onetime@6.0.0: 395 | resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} 396 | engines: {node: '>=12'} 397 | 398 | path-key@3.1.1: 399 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 400 | engines: {node: '>=8'} 401 | 402 | path-key@4.0.0: 403 | resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} 404 | engines: {node: '>=12'} 405 | 406 | pathe@1.1.2: 407 | resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} 408 | 409 | pathval@2.0.0: 410 | resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} 411 | engines: {node: '>= 14.16'} 412 | 413 | picocolors@1.0.1: 414 | resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} 415 | 416 | postcss@8.4.41: 417 | resolution: {integrity: sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==} 418 | engines: {node: ^10 || ^12 || >=14} 419 | 420 | rollup@4.20.0: 421 | resolution: {integrity: sha512-6rbWBChcnSGzIlXeIdNIZTopKYad8ZG8ajhl78lGRLsI2rX8IkaotQhVas2Ma+GPxJav19wrSzvRvuiv0YKzWw==} 422 | engines: {node: '>=18.0.0', npm: '>=8.0.0'} 423 | hasBin: true 424 | 425 | shebang-command@2.0.0: 426 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 427 | engines: {node: '>=8'} 428 | 429 | shebang-regex@3.0.0: 430 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 431 | engines: {node: '>=8'} 432 | 433 | siginfo@2.0.0: 434 | resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} 435 | 436 | signal-exit@4.1.0: 437 | resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} 438 | engines: {node: '>=14'} 439 | 440 | source-map-js@1.2.0: 441 | resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} 442 | engines: {node: '>=0.10.0'} 443 | 444 | stackback@0.0.2: 445 | resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} 446 | 447 | std-env@3.7.0: 448 | resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} 449 | 450 | strip-final-newline@3.0.0: 451 | resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} 452 | engines: {node: '>=12'} 453 | 454 | tinybench@2.9.0: 455 | resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} 456 | 457 | tinypool@1.0.0: 458 | resolution: {integrity: sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==} 459 | engines: {node: ^18.0.0 || >=20.0.0} 460 | 461 | tinyrainbow@1.2.0: 462 | resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} 463 | engines: {node: '>=14.0.0'} 464 | 465 | tinyspy@3.0.0: 466 | resolution: {integrity: sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==} 467 | engines: {node: '>=14.0.0'} 468 | 469 | typescript@5.5.4: 470 | resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} 471 | engines: {node: '>=14.17'} 472 | hasBin: true 473 | 474 | undici-types@6.13.0: 475 | resolution: {integrity: sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==} 476 | 477 | vite-node@2.0.5: 478 | resolution: {integrity: sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==} 479 | engines: {node: ^18.0.0 || >=20.0.0} 480 | hasBin: true 481 | 482 | vite@5.4.0: 483 | resolution: {integrity: sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==} 484 | engines: {node: ^18.0.0 || >=20.0.0} 485 | hasBin: true 486 | peerDependencies: 487 | '@types/node': ^18.0.0 || >=20.0.0 488 | less: '*' 489 | lightningcss: ^1.21.0 490 | sass: '*' 491 | sass-embedded: '*' 492 | stylus: '*' 493 | sugarss: '*' 494 | terser: ^5.4.0 495 | peerDependenciesMeta: 496 | '@types/node': 497 | optional: true 498 | less: 499 | optional: true 500 | lightningcss: 501 | optional: true 502 | sass: 503 | optional: true 504 | sass-embedded: 505 | optional: true 506 | stylus: 507 | optional: true 508 | sugarss: 509 | optional: true 510 | terser: 511 | optional: true 512 | 513 | vitest@2.0.5: 514 | resolution: {integrity: sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==} 515 | engines: {node: ^18.0.0 || >=20.0.0} 516 | hasBin: true 517 | peerDependencies: 518 | '@edge-runtime/vm': '*' 519 | '@types/node': ^18.0.0 || >=20.0.0 520 | '@vitest/browser': 2.0.5 521 | '@vitest/ui': 2.0.5 522 | happy-dom: '*' 523 | jsdom: '*' 524 | peerDependenciesMeta: 525 | '@edge-runtime/vm': 526 | optional: true 527 | '@types/node': 528 | optional: true 529 | '@vitest/browser': 530 | optional: true 531 | '@vitest/ui': 532 | optional: true 533 | happy-dom: 534 | optional: true 535 | jsdom: 536 | optional: true 537 | 538 | webidl-conversions@7.0.0: 539 | resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} 540 | engines: {node: '>=12'} 541 | 542 | whatwg-mimetype@3.0.0: 543 | resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} 544 | engines: {node: '>=12'} 545 | 546 | which@2.0.2: 547 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 548 | engines: {node: '>= 8'} 549 | hasBin: true 550 | 551 | why-is-node-running@2.3.0: 552 | resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} 553 | engines: {node: '>=8'} 554 | hasBin: true 555 | 556 | snapshots: 557 | 558 | '@ampproject/remapping@2.3.0': 559 | dependencies: 560 | '@jridgewell/gen-mapping': 0.3.5 561 | '@jridgewell/trace-mapping': 0.3.25 562 | 563 | '@esbuild/aix-ppc64@0.21.5': 564 | optional: true 565 | 566 | '@esbuild/android-arm64@0.21.5': 567 | optional: true 568 | 569 | '@esbuild/android-arm@0.21.5': 570 | optional: true 571 | 572 | '@esbuild/android-x64@0.21.5': 573 | optional: true 574 | 575 | '@esbuild/darwin-arm64@0.21.5': 576 | optional: true 577 | 578 | '@esbuild/darwin-x64@0.21.5': 579 | optional: true 580 | 581 | '@esbuild/freebsd-arm64@0.21.5': 582 | optional: true 583 | 584 | '@esbuild/freebsd-x64@0.21.5': 585 | optional: true 586 | 587 | '@esbuild/linux-arm64@0.21.5': 588 | optional: true 589 | 590 | '@esbuild/linux-arm@0.21.5': 591 | optional: true 592 | 593 | '@esbuild/linux-ia32@0.21.5': 594 | optional: true 595 | 596 | '@esbuild/linux-loong64@0.21.5': 597 | optional: true 598 | 599 | '@esbuild/linux-mips64el@0.21.5': 600 | optional: true 601 | 602 | '@esbuild/linux-ppc64@0.21.5': 603 | optional: true 604 | 605 | '@esbuild/linux-riscv64@0.21.5': 606 | optional: true 607 | 608 | '@esbuild/linux-s390x@0.21.5': 609 | optional: true 610 | 611 | '@esbuild/linux-x64@0.21.5': 612 | optional: true 613 | 614 | '@esbuild/netbsd-x64@0.21.5': 615 | optional: true 616 | 617 | '@esbuild/openbsd-x64@0.21.5': 618 | optional: true 619 | 620 | '@esbuild/sunos-x64@0.21.5': 621 | optional: true 622 | 623 | '@esbuild/win32-arm64@0.21.5': 624 | optional: true 625 | 626 | '@esbuild/win32-ia32@0.21.5': 627 | optional: true 628 | 629 | '@esbuild/win32-x64@0.21.5': 630 | optional: true 631 | 632 | '@jridgewell/gen-mapping@0.3.5': 633 | dependencies: 634 | '@jridgewell/set-array': 1.2.1 635 | '@jridgewell/sourcemap-codec': 1.5.0 636 | '@jridgewell/trace-mapping': 0.3.25 637 | 638 | '@jridgewell/resolve-uri@3.1.2': {} 639 | 640 | '@jridgewell/set-array@1.2.1': {} 641 | 642 | '@jridgewell/sourcemap-codec@1.5.0': {} 643 | 644 | '@jridgewell/trace-mapping@0.3.25': 645 | dependencies: 646 | '@jridgewell/resolve-uri': 3.1.2 647 | '@jridgewell/sourcemap-codec': 1.5.0 648 | 649 | '@rollup/rollup-android-arm-eabi@4.20.0': 650 | optional: true 651 | 652 | '@rollup/rollup-android-arm64@4.20.0': 653 | optional: true 654 | 655 | '@rollup/rollup-darwin-arm64@4.20.0': 656 | optional: true 657 | 658 | '@rollup/rollup-darwin-x64@4.20.0': 659 | optional: true 660 | 661 | '@rollup/rollup-linux-arm-gnueabihf@4.20.0': 662 | optional: true 663 | 664 | '@rollup/rollup-linux-arm-musleabihf@4.20.0': 665 | optional: true 666 | 667 | '@rollup/rollup-linux-arm64-gnu@4.20.0': 668 | optional: true 669 | 670 | '@rollup/rollup-linux-arm64-musl@4.20.0': 671 | optional: true 672 | 673 | '@rollup/rollup-linux-powerpc64le-gnu@4.20.0': 674 | optional: true 675 | 676 | '@rollup/rollup-linux-riscv64-gnu@4.20.0': 677 | optional: true 678 | 679 | '@rollup/rollup-linux-s390x-gnu@4.20.0': 680 | optional: true 681 | 682 | '@rollup/rollup-linux-x64-gnu@4.20.0': 683 | optional: true 684 | 685 | '@rollup/rollup-linux-x64-musl@4.20.0': 686 | optional: true 687 | 688 | '@rollup/rollup-win32-arm64-msvc@4.20.0': 689 | optional: true 690 | 691 | '@rollup/rollup-win32-ia32-msvc@4.20.0': 692 | optional: true 693 | 694 | '@rollup/rollup-win32-x64-msvc@4.20.0': 695 | optional: true 696 | 697 | '@types/estree@1.0.5': {} 698 | 699 | '@types/node@22.2.0': 700 | dependencies: 701 | undici-types: 6.13.0 702 | 703 | '@vitest/expect@2.0.5': 704 | dependencies: 705 | '@vitest/spy': 2.0.5 706 | '@vitest/utils': 2.0.5 707 | chai: 5.1.1 708 | tinyrainbow: 1.2.0 709 | 710 | '@vitest/pretty-format@2.0.5': 711 | dependencies: 712 | tinyrainbow: 1.2.0 713 | 714 | '@vitest/runner@2.0.5': 715 | dependencies: 716 | '@vitest/utils': 2.0.5 717 | pathe: 1.1.2 718 | 719 | '@vitest/snapshot@2.0.5': 720 | dependencies: 721 | '@vitest/pretty-format': 2.0.5 722 | magic-string: 0.30.11 723 | pathe: 1.1.2 724 | 725 | '@vitest/spy@2.0.5': 726 | dependencies: 727 | tinyspy: 3.0.0 728 | 729 | '@vitest/utils@2.0.5': 730 | dependencies: 731 | '@vitest/pretty-format': 2.0.5 732 | estree-walker: 3.0.3 733 | loupe: 3.1.1 734 | tinyrainbow: 1.2.0 735 | 736 | assertion-error@2.0.1: {} 737 | 738 | cac@6.7.14: {} 739 | 740 | chai@5.1.1: 741 | dependencies: 742 | assertion-error: 2.0.1 743 | check-error: 2.1.1 744 | deep-eql: 5.0.2 745 | loupe: 3.1.1 746 | pathval: 2.0.0 747 | 748 | check-error@2.1.1: {} 749 | 750 | cross-spawn@7.0.3: 751 | dependencies: 752 | path-key: 3.1.1 753 | shebang-command: 2.0.0 754 | which: 2.0.2 755 | 756 | debug@4.3.6: 757 | dependencies: 758 | ms: 2.1.2 759 | 760 | deep-eql@5.0.2: {} 761 | 762 | entities@4.5.0: {} 763 | 764 | esbuild@0.21.5: 765 | optionalDependencies: 766 | '@esbuild/aix-ppc64': 0.21.5 767 | '@esbuild/android-arm': 0.21.5 768 | '@esbuild/android-arm64': 0.21.5 769 | '@esbuild/android-x64': 0.21.5 770 | '@esbuild/darwin-arm64': 0.21.5 771 | '@esbuild/darwin-x64': 0.21.5 772 | '@esbuild/freebsd-arm64': 0.21.5 773 | '@esbuild/freebsd-x64': 0.21.5 774 | '@esbuild/linux-arm': 0.21.5 775 | '@esbuild/linux-arm64': 0.21.5 776 | '@esbuild/linux-ia32': 0.21.5 777 | '@esbuild/linux-loong64': 0.21.5 778 | '@esbuild/linux-mips64el': 0.21.5 779 | '@esbuild/linux-ppc64': 0.21.5 780 | '@esbuild/linux-riscv64': 0.21.5 781 | '@esbuild/linux-s390x': 0.21.5 782 | '@esbuild/linux-x64': 0.21.5 783 | '@esbuild/netbsd-x64': 0.21.5 784 | '@esbuild/openbsd-x64': 0.21.5 785 | '@esbuild/sunos-x64': 0.21.5 786 | '@esbuild/win32-arm64': 0.21.5 787 | '@esbuild/win32-ia32': 0.21.5 788 | '@esbuild/win32-x64': 0.21.5 789 | 790 | estree-walker@3.0.3: 791 | dependencies: 792 | '@types/estree': 1.0.5 793 | 794 | execa@8.0.1: 795 | dependencies: 796 | cross-spawn: 7.0.3 797 | get-stream: 8.0.1 798 | human-signals: 5.0.0 799 | is-stream: 3.0.0 800 | merge-stream: 2.0.0 801 | npm-run-path: 5.3.0 802 | onetime: 6.0.0 803 | signal-exit: 4.1.0 804 | strip-final-newline: 3.0.0 805 | 806 | fsevents@2.3.3: 807 | optional: true 808 | 809 | get-func-name@2.0.2: {} 810 | 811 | get-stream@8.0.1: {} 812 | 813 | happy-dom@14.12.3: 814 | dependencies: 815 | entities: 4.5.0 816 | webidl-conversions: 7.0.0 817 | whatwg-mimetype: 3.0.0 818 | 819 | human-signals@5.0.0: {} 820 | 821 | is-stream@3.0.0: {} 822 | 823 | isexe@2.0.0: {} 824 | 825 | loupe@3.1.1: 826 | dependencies: 827 | get-func-name: 2.0.2 828 | 829 | magic-string@0.30.11: 830 | dependencies: 831 | '@jridgewell/sourcemap-codec': 1.5.0 832 | 833 | merge-stream@2.0.0: {} 834 | 835 | mimic-fn@4.0.0: {} 836 | 837 | ms@2.1.2: {} 838 | 839 | nanoid@3.3.7: {} 840 | 841 | npm-run-path@5.3.0: 842 | dependencies: 843 | path-key: 4.0.0 844 | 845 | onetime@6.0.0: 846 | dependencies: 847 | mimic-fn: 4.0.0 848 | 849 | path-key@3.1.1: {} 850 | 851 | path-key@4.0.0: {} 852 | 853 | pathe@1.1.2: {} 854 | 855 | pathval@2.0.0: {} 856 | 857 | picocolors@1.0.1: {} 858 | 859 | postcss@8.4.41: 860 | dependencies: 861 | nanoid: 3.3.7 862 | picocolors: 1.0.1 863 | source-map-js: 1.2.0 864 | 865 | rollup@4.20.0: 866 | dependencies: 867 | '@types/estree': 1.0.5 868 | optionalDependencies: 869 | '@rollup/rollup-android-arm-eabi': 4.20.0 870 | '@rollup/rollup-android-arm64': 4.20.0 871 | '@rollup/rollup-darwin-arm64': 4.20.0 872 | '@rollup/rollup-darwin-x64': 4.20.0 873 | '@rollup/rollup-linux-arm-gnueabihf': 4.20.0 874 | '@rollup/rollup-linux-arm-musleabihf': 4.20.0 875 | '@rollup/rollup-linux-arm64-gnu': 4.20.0 876 | '@rollup/rollup-linux-arm64-musl': 4.20.0 877 | '@rollup/rollup-linux-powerpc64le-gnu': 4.20.0 878 | '@rollup/rollup-linux-riscv64-gnu': 4.20.0 879 | '@rollup/rollup-linux-s390x-gnu': 4.20.0 880 | '@rollup/rollup-linux-x64-gnu': 4.20.0 881 | '@rollup/rollup-linux-x64-musl': 4.20.0 882 | '@rollup/rollup-win32-arm64-msvc': 4.20.0 883 | '@rollup/rollup-win32-ia32-msvc': 4.20.0 884 | '@rollup/rollup-win32-x64-msvc': 4.20.0 885 | fsevents: 2.3.3 886 | 887 | shebang-command@2.0.0: 888 | dependencies: 889 | shebang-regex: 3.0.0 890 | 891 | shebang-regex@3.0.0: {} 892 | 893 | siginfo@2.0.0: {} 894 | 895 | signal-exit@4.1.0: {} 896 | 897 | source-map-js@1.2.0: {} 898 | 899 | stackback@0.0.2: {} 900 | 901 | std-env@3.7.0: {} 902 | 903 | strip-final-newline@3.0.0: {} 904 | 905 | tinybench@2.9.0: {} 906 | 907 | tinypool@1.0.0: {} 908 | 909 | tinyrainbow@1.2.0: {} 910 | 911 | tinyspy@3.0.0: {} 912 | 913 | typescript@5.5.4: {} 914 | 915 | undici-types@6.13.0: {} 916 | 917 | vite-node@2.0.5(@types/node@22.2.0): 918 | dependencies: 919 | cac: 6.7.14 920 | debug: 4.3.6 921 | pathe: 1.1.2 922 | tinyrainbow: 1.2.0 923 | vite: 5.4.0(@types/node@22.2.0) 924 | transitivePeerDependencies: 925 | - '@types/node' 926 | - less 927 | - lightningcss 928 | - sass 929 | - sass-embedded 930 | - stylus 931 | - sugarss 932 | - supports-color 933 | - terser 934 | 935 | vite@5.4.0(@types/node@22.2.0): 936 | dependencies: 937 | esbuild: 0.21.5 938 | postcss: 8.4.41 939 | rollup: 4.20.0 940 | optionalDependencies: 941 | '@types/node': 22.2.0 942 | fsevents: 2.3.3 943 | 944 | vitest@2.0.5(@types/node@22.2.0)(happy-dom@14.12.3): 945 | dependencies: 946 | '@ampproject/remapping': 2.3.0 947 | '@vitest/expect': 2.0.5 948 | '@vitest/pretty-format': 2.0.5 949 | '@vitest/runner': 2.0.5 950 | '@vitest/snapshot': 2.0.5 951 | '@vitest/spy': 2.0.5 952 | '@vitest/utils': 2.0.5 953 | chai: 5.1.1 954 | debug: 4.3.6 955 | execa: 8.0.1 956 | magic-string: 0.30.11 957 | pathe: 1.1.2 958 | std-env: 3.7.0 959 | tinybench: 2.9.0 960 | tinypool: 1.0.0 961 | tinyrainbow: 1.2.0 962 | vite: 5.4.0(@types/node@22.2.0) 963 | vite-node: 2.0.5(@types/node@22.2.0) 964 | why-is-node-running: 2.3.0 965 | optionalDependencies: 966 | '@types/node': 22.2.0 967 | happy-dom: 14.12.3 968 | transitivePeerDependencies: 969 | - less 970 | - lightningcss 971 | - sass 972 | - sass-embedded 973 | - stylus 974 | - sugarss 975 | - supports-color 976 | - terser 977 | 978 | webidl-conversions@7.0.0: {} 979 | 980 | whatwg-mimetype@3.0.0: {} 981 | 982 | which@2.0.2: 983 | dependencies: 984 | isexe: 2.0.0 985 | 986 | why-is-node-running@2.3.0: 987 | dependencies: 988 | siginfo: 2.0.0 989 | stackback: 0.0.2 990 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { TypedFormData } from "./typed-formdata.js"; 2 | export { parseFormDataRequest, parseFormData } from "./parsers.js"; 3 | -------------------------------------------------------------------------------- /src/parsers.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, expectTypeOf, it } from "vitest"; 2 | import { parseFormData, parseFormDataRequest } from "./parsers.js"; 3 | import { TypedFormData } from "./typed-formdata.js"; 4 | 5 | type TestForm = { 6 | foo: string; 7 | baz: string; 8 | file: File; 9 | }; 10 | 11 | describe("parseFormDataRequest", () => { 12 | it("should parse into TypedFormData from Request", async () => { 13 | const formData = new FormData(); 14 | formData.append("foo", "bar"); 15 | formData.append("baz", "qux"); 16 | formData.append("file", new File([], "file.txt")); 17 | 18 | const request = new Request("http://localhost", { 19 | method: "POST", 20 | body: formData, 21 | }); 22 | 23 | const parsed = await parseFormDataRequest(request); 24 | 25 | expect(parsed).toBeInstanceOf(TypedFormData); 26 | expect(parsed.get("foo")).toBe("bar"); 27 | expect(parsed.get("baz")).toBe("qux"); 28 | expectTypeOf(parsed.get("file")).toEqualTypeOf(); 29 | }); 30 | }); 31 | 32 | describe("parseFormData", () => { 33 | it("should parse into TypedFormData from FormData", () => { 34 | const formData = new FormData(); 35 | formData.append("foo", "bar"); 36 | formData.append("baz", "qux"); 37 | formData.append("file", new File([], "file.txt")); 38 | 39 | const parsed = parseFormData(formData); 40 | 41 | expect(parsed).toBeInstanceOf(TypedFormData); 42 | expect(parsed.get("foo")).toBe("bar"); 43 | expect(parsed.get("baz")).toBe("qux"); 44 | expectTypeOf(parsed.get("file")).toEqualTypeOf(); 45 | }); 46 | it("should parse into TypedFormData from Browser FormData", () => { 47 | const form = document.createElement("form"); 48 | const input = document.createElement("input"); 49 | input.name = "foo"; 50 | input.value = "bar"; 51 | form.appendChild(input); 52 | 53 | const formData = new FormData(form); 54 | const parsed = parseFormData(formData); 55 | 56 | expect(parsed).toBeInstanceOf(TypedFormData); 57 | expect(parsed.get("foo")).toBe("bar"); 58 | expectTypeOf(parsed.get("baz")).toEqualTypeOf(); 59 | expectTypeOf(parsed.get("file")).toEqualTypeOf(); 60 | }); 61 | }); 62 | 63 | -------------------------------------------------------------------------------- /src/parsers.ts: -------------------------------------------------------------------------------- 1 | import { TypedFormData } from "./typed-formdata.js"; 2 | 3 | export async function parseFormDataRequest< 4 | T extends Record 5 | >(request: Request): Promise> { 6 | const formData = await request.formData(); 7 | return new TypedFormData(formData); 8 | } 9 | 10 | export function parseFormData>( 11 | formData: FormData 12 | ): TypedFormData { 13 | return new TypedFormData(formData); 14 | } 15 | -------------------------------------------------------------------------------- /src/type-formdata.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeEach, expectTypeOf } from "vitest"; 2 | import { TypedFormData } from "./typed-formdata.js"; 3 | 4 | describe("TypedFormData", () => { 5 | type TestForm = { 6 | name: string; 7 | age: string; 8 | avatar: File; 9 | }; 10 | 11 | let typedFormData: TypedFormData; 12 | 13 | beforeEach(() => { 14 | typedFormData = new TypedFormData(); 15 | }); 16 | 17 | describe("native FormData behavior", () => { 18 | it(".set() should set and .get() should get values", () => { 19 | typedFormData.set("name", "John Doe"); 20 | typedFormData.set("age", "30"); 21 | 22 | expect(typedFormData.get("name")).toBe("John Doe"); 23 | expect(typedFormData.get("age")).toBe("30"); 24 | }); 25 | 26 | it(".append()should append values", () => { 27 | typedFormData.append("name", "John"); 28 | typedFormData.append("name", "Doe"); 29 | 30 | expect(typedFormData.getAll("name")).toEqual(["John", "Doe"]); 31 | }); 32 | 33 | it(".get() should return null for non-existent key", () => { 34 | expect(typedFormData.get("name")).toBeNull(); 35 | }); 36 | 37 | it(".get() should return the first value for a key with multiple values", () => { 38 | typedFormData.append("name", "John"); 39 | typedFormData.append("name", "Doe"); 40 | expect(typedFormData.get("name")).toBe("John"); 41 | }); 42 | 43 | it(".get() should return correct value for the key with one value", () => { 44 | typedFormData.set("name", "John"); 45 | expect(typedFormData.get("name")).toBe("John"); 46 | }); 47 | 48 | it(".has()should check if key exists", () => { 49 | typedFormData.set("name", "John"); 50 | 51 | expect(typedFormData.has("name")).toBe(true); 52 | expect(typedFormData.has("age")).toBe(false); 53 | }); 54 | 55 | it(".delete() should delete key", () => { 56 | typedFormData.set("name", "John"); 57 | typedFormData.delete("name"); 58 | 59 | expect(typedFormData.has("name")).toBe(false); 60 | }); 61 | 62 | it(".entries() should return entries", () => { 63 | typedFormData.set("name", "John"); 64 | typedFormData.set("age", "30"); 65 | 66 | const entries = Array.from(typedFormData.entries()); 67 | expect(entries).toEqual([ 68 | ["name", "John"], 69 | ["age", "30"], 70 | ]); 71 | }); 72 | 73 | it(".keys() should return only keys", () => { 74 | typedFormData.set("name", "John"); 75 | typedFormData.set("age", "30"); 76 | 77 | const keys = Array.from(typedFormData.keys()); 78 | expect(keys).toEqual(["name", "age"]); 79 | }); 80 | 81 | it(".values() should return only values", () => { 82 | typedFormData.set("name", "John"); 83 | typedFormData.set("age", "30"); 84 | 85 | const values = Array.from(typedFormData.values()); 86 | expect(values).toEqual(["John", "30"]); 87 | }); 88 | }); 89 | 90 | describe("TypedFormData constructor", () => { 91 | it("should create an empty TypedFormData when no argument is provided", () => { 92 | const typedFormData = new TypedFormData(); 93 | expect(Array.from(typedFormData.entries())).toEqual([]); 94 | }); 95 | 96 | it("should initialize from HTMLFormElement", () => { 97 | const form = document.createElement("form"); 98 | const input = document.createElement("input"); 99 | input.name = "name"; 100 | input.value = "John Doe"; 101 | form.appendChild(input); 102 | 103 | const typedFormData = new TypedFormData(form); 104 | expect(typedFormData.get("name")).toBe("John Doe"); 105 | }); 106 | 107 | it("should initialize from FormData", () => { 108 | const formData = new FormData(); 109 | formData.append("name", "John Doe"); 110 | formData.append("age", "30"); 111 | 112 | const typedFormData = new TypedFormData(formData); 113 | expect(typedFormData.get("name")).toBe("John Doe"); 114 | expect(typedFormData.get("age")).toBe("30"); 115 | }); 116 | }); 117 | 118 | describe("extendes TypedFormData methods", () => { 119 | it(".getObject() should return object representation", () => { 120 | typedFormData.set("name", "John"); 121 | typedFormData.set("age", "30"); 122 | 123 | expect(typedFormData.getObject()).toEqual({ name: "John", age: "30" }); 124 | }); 125 | it(".getObject() should return last value if many keys are set", () => { 126 | typedFormData.set("name", "John"); 127 | typedFormData.set("age", "30"); 128 | typedFormData.append("name", "John Doe"); 129 | 130 | expect(typedFormData.getObject()).toEqual({ 131 | name: "John Doe", 132 | age: "30", 133 | }); 134 | }); 135 | it(".getFormData() should return FormData", () => { 136 | typedFormData.set("name", "John"); 137 | typedFormData.set("age", "30"); 138 | 139 | expect(typedFormData.getFormData()).toBeInstanceOf(FormData); 140 | }); 141 | it(".typedEntries() should return entries", () => { 142 | typedFormData.set("name", "John"); 143 | typedFormData.set("age", "30"); 144 | 145 | const entries = Array.from(typedFormData.typedEntries()); 146 | expect(entries).toEqual([ 147 | ["name", "John"], 148 | ["age", "30"], 149 | ]); 150 | }); 151 | }); 152 | 153 | describe("types of TypedFormData methods", () => { 154 | describe("get()", () => { 155 | it(".get() of string key should return string", () => { 156 | typedFormData.set("name", "John"); 157 | expectTypeOf(typedFormData.get("name")).toEqualTypeOf(); 158 | }); 159 | it(".get() of file key should return File", () => { 160 | typedFormData.set("avatar", new File([], "avatar.png")); 161 | expectTypeOf(typedFormData.get("avatar")).toEqualTypeOf(); 162 | }); 163 | it(".get() first argument should be keys of TestForm", () => { 164 | typedFormData.set("name", "John"); 165 | expectTypeOf(typedFormData.get) 166 | .parameter(0) 167 | .toEqualTypeOf(); 168 | expectTypeOf(typedFormData.get) 169 | .parameter(0) 170 | .toEqualTypeOf<"name" | "age" | "avatar">(); 171 | }); 172 | it(".get() first argument shout not be other keys", () => { 173 | typedFormData.set("name", "John"); 174 | expectTypeOf(typedFormData.get) 175 | .parameter(0) 176 | .not.toEqualTypeOf<"other">(); 177 | }); 178 | }); 179 | describe(".set()", () => { 180 | it(".set() first argument should be keys of TestForm", () => { 181 | typedFormData.set("name", "John"); 182 | expectTypeOf(typedFormData.set) 183 | .parameter(0) 184 | .toEqualTypeOf(); 185 | expectTypeOf(typedFormData.set) 186 | .parameter(0) 187 | .toEqualTypeOf<"name" | "age" | "avatar">(); 188 | }); 189 | it(".set() first argument shout not be other keys", () => { 190 | typedFormData.set("name", "John"); 191 | expectTypeOf(typedFormData.set) 192 | .parameter(0) 193 | .not.toEqualTypeOf<"other">(); 194 | }); 195 | it.todo( 196 | ".set() second argument should be value for the key of TestForm", 197 | () => { 198 | // This can be impproved to handle situation when value is File 199 | typedFormData.set("name", "John"); 200 | } 201 | ); 202 | }); 203 | 204 | describe(".append()", () => { 205 | it(".append() first argument should be keys of TestForm", () => { 206 | typedFormData.append("name", "John"); 207 | expectTypeOf(typedFormData.append) 208 | .parameter(0) 209 | .toEqualTypeOf(); 210 | expectTypeOf(typedFormData.append) 211 | .parameter(0) 212 | .toEqualTypeOf<"name" | "age" | "avatar">(); 213 | }); 214 | it.todo( 215 | ".append() second argument should be value for the key of TestForm", 216 | () => { 217 | // This can be impproved to handle situation when value is File 218 | typedFormData.append("name", "John"); 219 | } 220 | ); 221 | }); 222 | 223 | describe(".has()", () => { 224 | it(".has() first argument should be keys of TestForm", () => { 225 | typedFormData.set("name", "John"); 226 | expectTypeOf(typedFormData.has) 227 | .parameter(0) 228 | .toEqualTypeOf(); 229 | expectTypeOf(typedFormData.has) 230 | .parameter(0) 231 | .toEqualTypeOf<"name" | "age" | "avatar">(); 232 | }); 233 | }); 234 | 235 | describe(".delete()", () => { 236 | it(".delete() first argument should be keys of TestForm", () => { 237 | typedFormData.set("name", "John"); 238 | expectTypeOf(typedFormData.delete) 239 | .parameter(0) 240 | .toEqualTypeOf(); 241 | expectTypeOf(typedFormData.delete) 242 | .parameter(0) 243 | .toEqualTypeOf<"name" | "age" | "avatar">(); 244 | }); 245 | }); 246 | 247 | describe(".typedEntries()", () => { 248 | it(".typedEntries() should return typed entries", () => { 249 | typedFormData.set("name", "John"); 250 | typedFormData.set("age", "30"); 251 | 252 | const entries = Array.from(typedFormData.typedEntries()); 253 | expectTypeOf(entries).toEqualTypeOf< 254 | [keyof TestForm, TestForm[keyof TestForm]][] 255 | >(); 256 | }); 257 | }); 258 | 259 | describe("Fetch API body", () => { 260 | it("should take TypedFormData as body", () => { 261 | const typedFormData = new TypedFormData(); 262 | typedFormData.set("name", "John"); 263 | typedFormData.set("age", "30"); 264 | 265 | fetch("https://example.com", { 266 | method: "POST", 267 | body: typedFormData, 268 | }); 269 | }); 270 | }); 271 | 272 | describe("In place of FormData in any functions", () => { 273 | it("should allow to use TypedFormData in any functions", () => { 274 | const typedFormData = new TypedFormData(); 275 | typedFormData.set("name", "John"); 276 | typedFormData.set("age", "30"); 277 | 278 | function test(formData: FormData) { 279 | console.log(formData.get("name")); 280 | } 281 | 282 | // INFO: should not be ts error - if not red thats good 283 | test(typedFormData); 284 | expectTypeOf(test).toBeCallableWith(typedFormData); 285 | }); 286 | }); 287 | }); 288 | }); 289 | -------------------------------------------------------------------------------- /src/typed-formdata.ts: -------------------------------------------------------------------------------- 1 | export class TypedFormData> 2 | implements FormData 3 | { 4 | private formData: FormData = new FormData(); 5 | 6 | constructor(initElement?: HTMLFormElement | FormData) { 7 | if (!initElement) { 8 | return; 9 | } 10 | if (initElement instanceof FormData) { 11 | this.formData = initElement; 12 | return; 13 | } 14 | 15 | this.formData = new FormData(initElement); 16 | } 17 | 18 | public get(key: Extract): T[K] | null { 19 | return this.formData.get(key) as T[K] | null; 20 | } 21 | 22 | public getAll(key: string): FormDataEntryValue[] { 23 | return this.formData.getAll(key); 24 | } 25 | 26 | /** 27 | * Executes a provided function once for each key/value pair in the FormData object. 28 | * @deprecated This method is deprecated and is not advised to be used. Use entries() or for...of loop instead. 29 | * @param callbackfn A function that is called for each key/value pair in the FormData object. 30 | * @param thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value. 31 | */ 32 | public forEach( 33 | callbackfn: ( 34 | value: FormDataEntryValue, 35 | key: string, 36 | parent: FormData 37 | ) => void, 38 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 39 | thisArg?: unknown 40 | ): void { 41 | this.formData.forEach(callbackfn, thisArg); 42 | } 43 | 44 | public getFormData(): FormData { 45 | return this.formData; 46 | } 47 | 48 | public getObject(): T { 49 | return Object.fromEntries(this.entries()) as T; 50 | } 51 | 52 | public entries(): IterableIterator<[string, FormDataEntryValue]> { 53 | return this.formData.entries(); 54 | } 55 | 56 | public typedEntries(): IterableIterator<[keyof T, T[keyof T]]> { 57 | return this.entries() as IterableIterator<[keyof T, T[keyof T]]>; 58 | } 59 | 60 | public keys(): IterableIterator { 61 | return this.formData.keys(); 62 | } 63 | 64 | public values(): IterableIterator { 65 | return this.formData.values(); 66 | } 67 | 68 | public set( 69 | key: keyof T, 70 | value: T[keyof T] extends File ? Blob : string 71 | ): void; 72 | public set(key: keyof T, value: string): void; 73 | public set(key: keyof T, value: Blob): void; 74 | public set( 75 | key: Extract, 76 | value: string | Blob, 77 | filename?: string 78 | ): void { 79 | if (typeof value === "string") { 80 | this.formData.set(key, value); 81 | } else { 82 | this.formData.set(key, value, filename); 83 | } 84 | } 85 | 86 | public append( 87 | key: keyof T, 88 | value: T[keyof T] extends File ? Blob : string 89 | ): void; 90 | public append(key: keyof T, value: string): void; 91 | public append(key: keyof T, value: Blob): void; 92 | public append( 93 | key: Extract, 94 | value: string | Blob, 95 | filename?: string 96 | ): void { 97 | if (typeof value === "string") { 98 | this.formData.append(key, value); 99 | } else { 100 | this.formData.append(key, value, filename); 101 | } 102 | } 103 | 104 | public has(key: Extract): boolean { 105 | return this.formData.has(key); 106 | } 107 | 108 | public delete(key: Extract): void { 109 | this.formData.delete(key); 110 | } 111 | 112 | public *[Symbol.iterator](): IterableIterator<[string, FormDataEntryValue]> { 113 | yield* this.entries(); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["DOM", "ES2020", "DOM.Iterable"], 4 | "module": "NodeNext", 5 | "target": "ES6", 6 | "moduleResolution": "NodeNext", 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "baseUrl": "src" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*.ts"], 4 | // "exclude": ["src/**/*.spec.ts"], 5 | "compilerOptions": { 6 | "composite": true, 7 | "rootDir": "src" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*.spec.ts"], 4 | "compilerOptions": { 5 | "noEmit": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from "vite"; 3 | 4 | export default defineConfig({ 5 | test: { 6 | typecheck: { 7 | enabled: true, 8 | include: ["src/**/*.spec.ts"], 9 | tsconfig: "./tsconfig.test.json", 10 | checker: "tsc", 11 | }, 12 | }, 13 | }); 14 | --------------------------------------------------------------------------------