An educational pure functional programming library in TypeScript
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Refactor examples to remove early returns, mutable arrays, and if/else chains

+645 -13
+12 -13
examples/http-client/with-purus.ts
··· 108 108 console.log(`[Fetch] ${url}`) 109 109 110 110 fetch(url, { signal: controller.signal }) 111 - .then(async (response) => { 112 - if (response.ok) { 113 - const data: unknown = await response.json() 114 - // Use type guard instead of casting - validates at runtime 115 - isUser(data) 116 - ? resume(Exit.succeed(data)) 117 - : resume(Exit.fail(HttpError.serverError(500))) 118 - } else if (response.status === 404) { 119 - resume(Exit.fail(HttpError.notFound(url))) 120 - } else { 121 - resume(Exit.fail(HttpError.serverError(response.status))) 122 - } 123 - }) 111 + .then((response) => 112 + !response.ok 113 + ? response.status === 404 114 + ? resume(Exit.fail(HttpError.notFound(url))) 115 + : resume(Exit.fail(HttpError.serverError(response.status))) 116 + : response.json().then((data: unknown) => 117 + // Use type guard instead of casting - validates at runtime 118 + isUser(data) 119 + ? resume(Exit.succeed(data)) 120 + : resume(Exit.fail(HttpError.serverError(500))), 121 + ), 122 + ) 124 123 .catch((err) => { 125 124 // GOTCHA: Don't resume on AbortError - the fiber is already cancelled 126 125 // Resuming after cancellation would cause undefined behavior
+352
examples/user-registration/with-purus.ts
··· 1 + /** 2 + * User Registration Example - Form Validation & Batch Processing 3 + * ============================================================== 4 + * 5 + * This example teaches three purus concepts: 6 + * 7 + * 1. VALIDATION - Accumulate all errors, not just the first 8 + * Unlike Result which short-circuits, Validation collects ALL errors. 9 + * This is perfect for form validation where you want to show every issue. 10 + * 11 + * 2. TYPED ERROR CHANNELS - Validation vs API errors are distinct 12 + * ValidationError and ApiError are separate types. The compiler ensures 13 + * you handle each appropriately - no runtime type checking needed. 14 + * 15 + * 3. TRAVERSE - Process collections with effects 16 + * traverse/traversePar map an effect-producing function over a list 17 + * and collect results. traversePar runs all effects in parallel. 18 + * 19 + * Prerequisites: http-client example (Eff basics) 20 + * New concepts: Validation, apValidation, traverse, traversePar 21 + */ 22 + 23 + import { 24 + // Validation 25 + valid, 26 + invalidOne, 27 + apValidation, 28 + matchValidation, 29 + type Validation, 30 + // Effect 31 + type Eff, 32 + succeed, 33 + fail, 34 + flatMap, 35 + foldEff, 36 + mapEff, 37 + sleep, 38 + // Traverse 39 + traversePar, 40 + // Utilities 41 + pipe, 42 + runPromise, 43 + runPromiseExit, 44 + match, 45 + } from "../../src/index" 46 + 47 + // ============================================================================= 48 + // SECTION 1: Domain Types 49 + // ============================================================================= 50 + // 51 + // Same types as without-purus.ts. The difference is in how we use them. 52 + // ============================================================================= 53 + 54 + type User = { 55 + readonly id: string 56 + readonly name: string 57 + readonly email: string 58 + readonly age: number 59 + } 60 + 61 + type ValidationError = 62 + | { readonly _tag: "EmptyField"; readonly field: string } 63 + | { readonly _tag: "InvalidEmail"; readonly email: string } 64 + | { readonly _tag: "InvalidAge"; readonly age: number; readonly reason: string } 65 + 66 + type ApiError = 67 + | { readonly _tag: "NetworkError"; readonly message: string } 68 + | { readonly _tag: "DuplicateEmail"; readonly email: string } 69 + | { readonly _tag: "ServerError"; readonly status: number } 70 + 71 + // Smart constructors for errors 72 + const ValidationError = { 73 + emptyField: (field: string): ValidationError => ({ _tag: "EmptyField", field }), 74 + invalidEmail: (email: string): ValidationError => ({ _tag: "InvalidEmail", email }), 75 + invalidAge: (age: number, reason: string): ValidationError => ({ 76 + _tag: "InvalidAge", 77 + age, 78 + reason, 79 + }), 80 + } 81 + 82 + const ApiError = { 83 + network: (message: string): ApiError => ({ _tag: "NetworkError", message }), 84 + duplicateEmail: (email: string): ApiError => ({ _tag: "DuplicateEmail", email }), 85 + serverError: (status: number): ApiError => ({ _tag: "ServerError", status }), 86 + } 87 + 88 + // ============================================================================= 89 + // SECTION 2: Individual Validators 90 + // ============================================================================= 91 + // 92 + // KEY INSIGHT: Each validator returns Validation<A, E>, not Result<A, E>. 93 + // The difference? Validation accumulates errors via apValidation, while 94 + // Result short-circuits on the first error via flatMap. 95 + // 96 + // PATTERN: Use invalidOne() for a single error, valid() for success. 97 + // ============================================================================= 98 + 99 + const validateName = (name: string): Validation<string, ValidationError> => 100 + name.trim().length === 0 101 + ? invalidOne(ValidationError.emptyField("name")) 102 + : valid(name.trim()) 103 + 104 + const validateEmail = (email: string): Validation<string, ValidationError> => 105 + email.trim().length === 0 106 + ? invalidOne(ValidationError.emptyField("email")) 107 + : /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim().toLowerCase()) 108 + ? valid(email.trim().toLowerCase()) 109 + : invalidOne(ValidationError.invalidEmail(email)) 110 + 111 + const validateAge = (age: number): Validation<number, ValidationError> => 112 + !Number.isInteger(age) 113 + ? invalidOne(ValidationError.invalidAge(age, "must be a whole number")) 114 + : age < 0 115 + ? invalidOne(ValidationError.invalidAge(age, "must be non-negative")) 116 + : age < 13 117 + ? invalidOne(ValidationError.invalidAge(age, "must be at least 13")) 118 + : valid(age) 119 + 120 + // ============================================================================= 121 + // SECTION 3: Combining Validators with apValidation 122 + // ============================================================================= 123 + // 124 + // THE MAGIC OF APPLICATIVE: 125 + // We start with a curried constructor: valid((name) => (email) => (age) => ...) 126 + // Then apply each validation with apValidation. If any fail, errors accumulate. 127 + // 128 + // Compare to monadic style (flatMap): 129 + // flatMap would stop at the FIRST error 130 + // apValidation collects ALL errors 131 + // 132 + // This is perfect for forms where you want to show every validation issue. 133 + // ============================================================================= 134 + 135 + type UserInput = Omit<User, "id"> 136 + 137 + const validateForm = ( 138 + name: string, 139 + email: string, 140 + age: number, 141 + ): Validation<UserInput, ValidationError> => 142 + // Start with a curried constructor function wrapped in valid() 143 + pipe( 144 + valid( 145 + (validName: string) => (validEmail: string) => (validAge: number): UserInput => ({ 146 + name: validName, 147 + email: validEmail, 148 + age: validAge, 149 + }), 150 + ), 151 + // Apply each validation - errors accumulate automatically! 152 + apValidation(validateName(name)), 153 + apValidation(validateEmail(email)), 154 + apValidation(validateAge(age)), 155 + ) 156 + 157 + // ============================================================================= 158 + // SECTION 4: API Calls as Effects 159 + // ============================================================================= 160 + // 161 + // WHY Eff<User, ApiError> INSTEAD OF Promise<User>: 162 + // - The error type ApiError is part of the signature - can't forget to handle it 163 + // - Effects are lazy - nothing runs until runPromise 164 + // - Composable with retry, timeout, race, etc. 165 + // 166 + // BRIDGING VALIDATION -> EFF: 167 + // Use matchValidation to convert: valid values become succeed(), invalid becomes fail(). 168 + // ============================================================================= 169 + 170 + const createUser = (input: UserInput): Eff<User, ApiError, unknown> => 171 + pipe( 172 + // Simulate API delay 173 + sleep(100), 174 + // Simulate various API failures 175 + flatMap(() => 176 + Math.random() < 0.1 177 + ? fail(ApiError.network("Connection refused")) 178 + : input.email === "taken@example.com" 179 + ? fail(ApiError.duplicateEmail(input.email)) 180 + : Math.random() < 0.05 181 + ? fail(ApiError.serverError(500)) 182 + : succeed({ 183 + id: `user_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`, 184 + ...input, 185 + }), 186 + ), 187 + ) 188 + 189 + // ============================================================================= 190 + // SECTION 5: Batch Processing with traversePar 191 + // ============================================================================= 192 + // 193 + // TRAVERSE PATTERN: 194 + // traverse(f)(list) = apply f to each element, collect results 195 + // - Sequential: traverse 196 + // - Parallel: traversePar 197 + // 198 + // Unlike Promise.allSettled which fails fast, we can use foldEff to 199 + // handle each result individually and collect successes AND failures. 200 + // 201 + // KEY INSIGHT: We wrap createUser with foldEff to convert failures into 202 + // successes containing the error. This way traversePar always succeeds, 203 + // and we can categorize results afterwards. 204 + // ============================================================================= 205 + 206 + type BatchResult = { 207 + readonly successful: User[] 208 + readonly failed: Array<{ input: UserInput; error: ApiError }> 209 + } 210 + 211 + const createUsersBatch = ( 212 + users: readonly UserInput[], 213 + ): Eff<BatchResult, never, unknown> => { 214 + // Wrap each createUser call to capture both success and failure 215 + const createAndCapture = ( 216 + input: UserInput, 217 + ): Eff<{ ok: true; user: User } | { ok: false; input: UserInput; error: ApiError }, never, unknown> => 218 + pipe( 219 + createUser(input), 220 + foldEff( 221 + // On failure: wrap error in success (so traversePar continues) 222 + (error) => succeed({ ok: false as const, input, error }), 223 + // On success: wrap user in success 224 + (user) => succeed({ ok: true as const, user }), 225 + ), 226 + ) 227 + 228 + // Run all in parallel and collect results 229 + return pipe( 230 + users, 231 + traversePar(createAndCapture), 232 + mapEff((results) => 233 + results.reduce<BatchResult>( 234 + (acc, result) => 235 + result.ok 236 + ? { ...acc, successful: [...acc.successful, result.user] } 237 + : { ...acc, failed: [...acc.failed, { input: result.input, error: result.error }] }, 238 + { successful: [], failed: [] }, 239 + ), 240 + ), 241 + ) 242 + } 243 + 244 + // ============================================================================= 245 + // SECTION 6: Error Formatting (Type-safe!) 246 + // ============================================================================= 247 + // 248 + // NO RUNTIME TYPE CHECKS: 249 + // With match(), TypeScript ensures we handle every variant. 250 + // Add a new error variant? Compiler error until you handle it. 251 + // ============================================================================= 252 + 253 + const formatValidationError = (error: ValidationError): string => 254 + match(error)({ 255 + EmptyField: ({ field }) => `${field} is required`, 256 + InvalidEmail: ({ email }) => `"${email}" is not a valid email`, 257 + InvalidAge: ({ age, reason }) => `Age ${age}: ${reason}`, 258 + }) 259 + 260 + const formatApiError = (error: ApiError): string => 261 + match(error)({ 262 + NetworkError: ({ message }) => `Network error: ${message}`, 263 + DuplicateEmail: ({ email }) => `Email already registered: ${email}`, 264 + ServerError: ({ status }) => `Server error (${status})`, 265 + }) 266 + 267 + // ============================================================================= 268 + // SECTION 7: Demo 269 + // ============================================================================= 270 + 271 + const main = async () => { 272 + console.log("=== User Registration (with purus) ===\n") 273 + 274 + // --------------------------------------------------------------------------- 275 + // Test 1: Invalid form - shows error accumulation 276 + // apValidation collects ALL errors, not just the first one 277 + // --------------------------------------------------------------------------- 278 + console.log("--- Test 1: Invalid form (multiple errors) ---") 279 + 280 + const result1 = validateForm("", "not-an-email", -5) 281 + 282 + matchValidation( 283 + (_data) => console.log("Unexpectedly valid!"), 284 + (errors) => { 285 + console.log("Validation errors:") 286 + errors.forEach((err) => console.log(` - ${formatValidationError(err)}`)) 287 + }, 288 + )(result1) 289 + console.log() 290 + 291 + // --------------------------------------------------------------------------- 292 + // Test 2: Valid form -> API call 293 + // Shows: matchValidation bridges Validation to Eff 294 + // --------------------------------------------------------------------------- 295 + console.log("--- Test 2: Valid form -> API call ---") 296 + 297 + const result2 = validateForm("Alice", "alice@example.com", 25) 298 + 299 + // Bridge Validation to Eff 300 + const apiCall = matchValidation( 301 + (data: UserInput) => { 302 + console.log("Form valid, calling API...") 303 + return createUser(data) 304 + }, 305 + (errors): Eff<User, ApiError, unknown> => { 306 + // Convert validation errors to API error for uniform handling 307 + const msg = errors.map(formatValidationError).join("; ") 308 + return fail(ApiError.network(`Validation failed: ${msg}`)) 309 + }, 310 + )(result2) 311 + 312 + const exit2 = await runPromiseExit(apiCall) 313 + 314 + match(exit2)({ 315 + Success: ({ value }) => console.log(`Created user: ${value.id} (${value.name})`), 316 + Failure: ({ error }) => console.log(`API failed: ${formatApiError(error)}`), 317 + Interrupted: ({ by }) => console.log(`Interrupted by: ${by}`), 318 + }) 319 + console.log() 320 + 321 + // --------------------------------------------------------------------------- 322 + // Test 3: Batch processing with traversePar 323 + // Shows: All effects run in parallel, results collected 324 + // --------------------------------------------------------------------------- 325 + console.log("--- Test 3: Batch create (5 users) ---") 326 + 327 + const testUsers: readonly UserInput[] = [ 328 + { name: "Bob", email: "bob@example.com", age: 30 }, 329 + { name: "Charlie", email: "taken@example.com", age: 28 }, // Will fail 330 + { name: "Diana", email: "diana@example.com", age: 35 }, 331 + { name: "Eve", email: "eve@example.com", age: 22 }, 332 + { name: "Frank", email: "frank@example.com", age: 40 }, 333 + ] 334 + 335 + console.log(`Creating ${testUsers.length} users in parallel with traversePar...`) 336 + const batchResult = await runPromise(createUsersBatch(testUsers)) 337 + 338 + console.log(`\nResults:`) 339 + console.log(` Successful: ${batchResult.successful.length}`) 340 + batchResult.successful.forEach((u) => console.log(` - ${u.name} (${u.id})`)) 341 + 342 + console.log(` Failed: ${batchResult.failed.length}`) 343 + batchResult.failed.forEach((f) => 344 + console.log(` - ${f.input.name}: ${formatApiError(f.error)}`), 345 + ) 346 + 347 + console.log("\n=== Done ===") 348 + } 349 + 350 + main() 351 + .catch(console.error) 352 + .finally(() => process.exit(0))
+281
examples/user-registration/without-purus.ts
··· 1 + /** 2 + * User Registration - Vanilla TypeScript (Comparison) 3 + * ==================================================== 4 + * 5 + * This is the "before" version. Compare with with-purus.ts to see how purus 6 + * improves form validation and batch processing. 7 + * 8 + * PROBLEMS THIS APPROACH HAS: 9 + * 10 + * 1. ERROR ACCUMULATION IS MANUAL - We must create an array, push errors, 11 + * check if it's empty. Easy to forget a case or return early by accident. 12 + * 13 + * 2. PROMISE.ALLSETTLED LOSES TYPE INFO - Results are { status, value } or 14 + * { status, reason } with `unknown` types. No compiler help. 15 + * 16 + * 3. TWO ERROR SYSTEMS - Validation errors are arrays, API errors are thrown. 17 + * Callers must handle both patterns differently. 18 + * 19 + * Prerequisites: Promise understanding, http-client example 20 + * Next: Compare with with-purus.ts to see the improvement 21 + */ 22 + 23 + // ============================================================================= 24 + // SECTION 1: Domain Types 25 + // ============================================================================= 26 + 27 + type User = { 28 + readonly id: string 29 + readonly name: string 30 + readonly email: string 31 + readonly age: number 32 + } 33 + 34 + type ValidationError = 35 + | { readonly _tag: "EmptyField"; readonly field: string } 36 + | { readonly _tag: "InvalidEmail"; readonly email: string } 37 + | { readonly _tag: "InvalidAge"; readonly age: number; readonly reason: string } 38 + 39 + type ApiError = 40 + | { readonly _tag: "NetworkError"; readonly message: string } 41 + | { readonly _tag: "DuplicateEmail"; readonly email: string } 42 + | { readonly _tag: "ServerError"; readonly status: number } 43 + 44 + // ============================================================================= 45 + // SECTION 2: Form Validation 46 + // ============================================================================= 47 + // 48 + // MANUAL ERROR ACCUMULATION: 49 + // We need to collect ALL validation errors, not just the first one. 50 + // This requires: 51 + // - A mutable array to push errors into 52 + // - Checking each field independently (no early returns!) 53 + // - Remembering to check the array length at the end 54 + // 55 + // GOTCHA: Easy to accidentally write `return` instead of pushing to array, 56 + // which would stop validation early and miss other errors. 57 + // ============================================================================= 58 + 59 + const validateForm = ( 60 + name: string, 61 + email: string, 62 + age: number, 63 + ): { valid: true; data: Omit<User, "id"> } | { valid: false; errors: ValidationError[] } => { 64 + // Manual error accumulation - we must track this ourselves 65 + const errors: ValidationError[] = [] 66 + 67 + // Validate name - push to array, don't return early! 68 + if (name.trim().length === 0) { 69 + errors.push({ _tag: "EmptyField", field: "name" }) 70 + } 71 + 72 + // Validate email - basic regex check 73 + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ 74 + if (email.trim().length === 0) { 75 + errors.push({ _tag: "EmptyField", field: "email" }) 76 + } else if (!emailRegex.test(email)) { 77 + errors.push({ _tag: "InvalidEmail", email }) 78 + } 79 + 80 + // Validate age 81 + if (!Number.isInteger(age)) { 82 + errors.push({ _tag: "InvalidAge", age, reason: "must be a whole number" }) 83 + } else if (age < 0) { 84 + errors.push({ _tag: "InvalidAge", age, reason: "must be non-negative" }) 85 + } else if (age < 13) { 86 + errors.push({ _tag: "InvalidAge", age, reason: "must be at least 13" }) 87 + } 88 + 89 + // Have to remember to check errors.length 90 + if (errors.length > 0) { 91 + return { valid: false, errors } 92 + } 93 + 94 + return { valid: true, data: { name: name.trim(), email: email.trim().toLowerCase(), age } } 95 + } 96 + 97 + // ============================================================================= 98 + // SECTION 3: API Calls (Simulated) 99 + // ============================================================================= 100 + // 101 + // ERRORS ARE THROWN: 102 + // Unlike validation errors which are returned, API errors are thrown. 103 + // This means callers must use try/catch AND handle the returned validation errors. 104 + // Two different error-handling patterns in one flow! 105 + // 106 + // GOTCHA: The thrown error is typed here, but in the catch block it becomes 107 + // `unknown`. We lose all type information at the call site. 108 + // ============================================================================= 109 + 110 + const simulateApiCall = async (data: Omit<User, "id">): Promise<User> => { 111 + // Simulate network delay 112 + await new Promise((resolve) => setTimeout(resolve, 100)) 113 + 114 + // Simulate various API failures 115 + if (Math.random() < 0.1) { 116 + throw { _tag: "NetworkError", message: "Connection refused" } as ApiError 117 + } 118 + 119 + if (data.email === "taken@example.com") { 120 + throw { _tag: "DuplicateEmail", email: data.email } as ApiError 121 + } 122 + 123 + if (Math.random() < 0.05) { 124 + throw { _tag: "ServerError", status: 500 } as ApiError 125 + } 126 + 127 + // Success - generate a user with an ID 128 + return { 129 + id: `user_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`, 130 + ...data, 131 + } 132 + } 133 + 134 + // ============================================================================= 135 + // SECTION 4: Batch Processing 136 + // ============================================================================= 137 + // 138 + // PROMISE.ALLSETTLED LOSES TYPES: 139 + // The result type is PromiseSettledResult<User>[], which has: 140 + // - { status: "fulfilled", value: User } 141 + // - { status: "rejected", reason: unknown } <-- unknown! 142 + // 143 + // We have to manually cast or check types at runtime. 144 + // ============================================================================= 145 + 146 + type BatchResult = { 147 + readonly successful: User[] 148 + readonly failed: Array<{ input: Omit<User, "id">; error: unknown }> 149 + } 150 + 151 + const createUsersBatch = async ( 152 + users: Array<Omit<User, "id">>, 153 + ): Promise<BatchResult> => { 154 + const results = await Promise.allSettled(users.map((u) => simulateApiCall(u))) 155 + 156 + // Manual categorization - lots of boilerplate 157 + const successful: User[] = [] 158 + const failed: Array<{ input: Omit<User, "id">; error: unknown }> = [] 159 + 160 + results.forEach((result, index) => { 161 + if (result.status === "fulfilled") { 162 + successful.push(result.value) 163 + } else { 164 + // result.reason is `unknown` - we lost the ApiError type! 165 + failed.push({ input: users[index]!, error: result.reason }) 166 + } 167 + }) 168 + 169 + return { successful, failed } 170 + } 171 + 172 + // ============================================================================= 173 + // SECTION 5: Error Formatting (Runtime Type Checking) 174 + // ============================================================================= 175 + // 176 + // Since errors become `unknown` in catch blocks, we need runtime checks. 177 + // This is fragile - if we add a new error variant, TypeScript won't remind us. 178 + // ============================================================================= 179 + 180 + const formatValidationError = (error: ValidationError): string => { 181 + switch (error._tag) { 182 + case "EmptyField": 183 + return `${error.field} is required` 184 + case "InvalidEmail": 185 + return `"${error.email}" is not a valid email` 186 + case "InvalidAge": 187 + return `Age ${error.age}: ${error.reason}` 188 + } 189 + } 190 + 191 + const formatApiError = (error: unknown): string => { 192 + // Type guard - we have to check at runtime 193 + if (typeof error !== "object" || error === null || !("_tag" in error)) { 194 + return `Unknown error: ${String(error)}` 195 + } 196 + 197 + const apiError = error as ApiError // Unsafe cast! 198 + switch (apiError._tag) { 199 + case "NetworkError": 200 + return `Network error: ${apiError.message}` 201 + case "DuplicateEmail": 202 + return `Email already registered: ${apiError.email}` 203 + case "ServerError": 204 + return `Server error (${apiError.status})` 205 + default: 206 + return `Unknown API error` 207 + } 208 + } 209 + 210 + // ============================================================================= 211 + // SECTION 6: Demo 212 + // ============================================================================= 213 + 214 + const main = async () => { 215 + console.log("=== User Registration (without purus) ===\n") 216 + 217 + // --------------------------------------------------------------------------- 218 + // Test 1: Invalid form - shows error accumulation 219 + // --------------------------------------------------------------------------- 220 + console.log("--- Test 1: Invalid form (multiple errors) ---") 221 + 222 + const result1 = validateForm("", "not-an-email", -5) 223 + 224 + if (!result1.valid) { 225 + console.log("Validation errors:") 226 + result1.errors.forEach((err) => console.log(` - ${formatValidationError(err)}`)) 227 + } 228 + console.log() 229 + 230 + // --------------------------------------------------------------------------- 231 + // Test 2: Valid form -> API call 232 + // Shows: Two different error patterns (validation + try/catch) 233 + // --------------------------------------------------------------------------- 234 + console.log("--- Test 2: Valid form -> API call ---") 235 + 236 + const result2 = validateForm("Alice", "alice@example.com", 25) 237 + 238 + if (result2.valid) { 239 + console.log("Form valid, calling API...") 240 + try { 241 + const user = await simulateApiCall(result2.data) 242 + console.log(`Created user: ${user.id} (${user.name})`) 243 + } catch (e: unknown) { 244 + // e is unknown - we lost the ApiError type! 245 + console.log(`API failed: ${formatApiError(e)}`) 246 + } 247 + } 248 + console.log() 249 + 250 + // --------------------------------------------------------------------------- 251 + // Test 3: Batch processing 252 + // Shows: Promise.allSettled with manual result categorization 253 + // --------------------------------------------------------------------------- 254 + console.log("--- Test 3: Batch create (5 users) ---") 255 + 256 + const testUsers: Array<Omit<User, "id">> = [ 257 + { name: "Bob", email: "bob@example.com", age: 30 }, 258 + { name: "Charlie", email: "taken@example.com", age: 28 }, // Will fail 259 + { name: "Diana", email: "diana@example.com", age: 35 }, 260 + { name: "Eve", email: "eve@example.com", age: 22 }, 261 + { name: "Frank", email: "frank@example.com", age: 40 }, 262 + ] 263 + 264 + console.log(`Creating ${testUsers.length} users in parallel...`) 265 + const batchResult = await createUsersBatch(testUsers) 266 + 267 + console.log(`\nResults:`) 268 + console.log(` Successful: ${batchResult.successful.length}`) 269 + batchResult.successful.forEach((u) => console.log(` - ${u.name} (${u.id})`)) 270 + 271 + console.log(` Failed: ${batchResult.failed.length}`) 272 + batchResult.failed.forEach((f) => 273 + console.log(` - ${f.input.name}: ${formatApiError(f.error)}`), 274 + ) 275 + 276 + console.log("\n=== Done ===") 277 + } 278 + 279 + main() 280 + .catch(console.error) 281 + .finally(() => process.exit(0))