An educational pure functional programming library in TypeScript
2
fork

Configure Feed

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

Add isEmpty/isNonEmpty/size predicates and isSet/isMap guards

+91 -9
+7
CHANGELOG.md
··· 2 2 3 3 All notable changes to this project will be documented in this file. 4 4 5 + ## [0.1.0-alpha.9] - 2026-02-07 6 + 7 + ### Added 8 + 9 + - **Emptiness predicates** — `isEmpty` / `isNonEmpty` for strings, arrays, Sets, Maps, and objects 10 + - Uses FP naming convention (`isNonEmpty` mirrors the `NonEmpty` type from Haskell/Scala/PureScript) 11 + 5 12 ## [0.1.0-alpha.8] - 2026-02-07 6 13 7 14 ### Added
+4 -2
docs-site/src/content/docs/stories/forest-election/01-the-ballot-box-problem.mdx
··· 157 157 matchValidation, 158 158 match, 159 159 isDefined, 160 + isEmpty, 161 + isNonEmpty, 160 162 pipe, 161 163 } from "purus-ts"; 162 164 ``` ··· 173 175 const validateVoter = ( 174 176 voter: string | undefined, 175 177 ): Validation<string, BallotError> => 176 - voter && voter.trim().length > 0 178 + voter && isNonEmpty(voter.trim()) 177 179 ? valid(voter.trim()) 178 180 : invalidOne({ _tag: "MissingVoter" }); 179 181 ``` ··· 192 194 const validateCandidate = ( 193 195 candidate: string | undefined, 194 196 ): Validation<Candidate, BallotError> => 195 - !candidate || candidate.trim().length === 0 197 + !candidate || isEmpty(candidate.trim()) 196 198 ? invalidOne({ _tag: "MissingCandidate" }) 197 199 : pipe( 198 200 VALID_CANDIDATES.find((c) => c === candidate.trim()),
+4 -2
examples/stories/forest-election/01-validation.ts
··· 21 21 matchValidation, 22 22 match, 23 23 isDefined, 24 + isEmpty, 25 + isNonEmpty, 24 26 pipe, 25 27 } from "../../../src/index" 26 28 ··· 53 55 const validateVoter = ( 54 56 voter: string | undefined, 55 57 ): Validation<string, BallotError> => 56 - voter && voter.trim().length > 0 58 + voter && isNonEmpty(voter.trim()) 57 59 ? valid(voter.trim()) 58 60 : invalidOne({ _tag: "MissingVoter" }) 59 61 ··· 63 65 const validateCandidate = ( 64 66 candidate: string | undefined, 65 67 ): Validation<Candidate, BallotError> => 66 - !candidate || candidate.trim().length === 0 68 + !candidate || isEmpty(candidate.trim()) 67 69 ? invalidOne({ _tag: "MissingCandidate" }) 68 70 : pipe( 69 71 VALID_CANDIDATES.find((c) => c === candidate.trim()),
+2 -1
examples/stories/forest-election/02-result.ts
··· 20 20 mapResult, 21 21 matchResult, 22 22 match, 23 + isEmpty, 23 24 pipe, 24 25 } from "../../../src/index" 25 26 ··· 77 78 const ensureNotEmpty = ( 78 79 ballots: readonly Ballot[], 79 80 ): Result<readonly Ballot[], CountError> => 80 - ballots.length === 0 ? err({ _tag: "EmptyBallotBox" }) : ok(ballots) 81 + isEmpty(ballots) ? err({ _tag: "EmptyBallotBox" }) : ok(ballots) 81 82 82 83 /** 83 84 * Verify that the counted votes match the number of ballots.
+2 -1
examples/stories/forest-election/03-effects.ts
··· 25 25 retry, 26 26 all, 27 27 match, 28 + isNonEmpty, 28 29 } from "../../../src/index" 29 30 30 31 // ============================================================================= ··· 217 218 .join("\n"), 218 219 ) 219 220 220 - if (failed.length > 0) { 221 + if (isNonEmpty(failed)) { 221 222 console.log(`\nFailed: ${failed.length}/4`) 222 223 console.log( 223 224 failed
+3 -2
examples/user-registration/with-purus.ts
··· 26 26 invalidOne, 27 27 apValidation, 28 28 matchValidation, 29 + isEmpty, 29 30 type Validation, 30 31 // Effect 31 32 type Eff, ··· 97 98 // ============================================================================= 98 99 99 100 const validateName = (name: string): Validation<string, ValidationError> => 100 - name.trim().length === 0 101 + isEmpty(name.trim()) 101 102 ? invalidOne(ValidationError.emptyField("name")) 102 103 : valid(name.trim()) 103 104 104 105 const validateEmail = (email: string): Validation<string, ValidationError> => 105 - email.trim().length === 0 106 + isEmpty(email.trim()) 106 107 ? invalidOne(ValidationError.emptyField("email")) 107 108 : /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim().toLowerCase()) 108 109 ? valid(email.trim().toLowerCase())
+1 -1
package.json
··· 1 1 { 2 2 "name": "purus-ts", 3 - "version": "0.1.0-alpha.8", 3 + "version": "0.1.0-alpha.9", 4 4 "description": "Pure TypeScript effect system with fiber-based concurrency, brands, refinements, and pattern matching", 5 5 "type": "module", 6 6 "main": "./dist/index.js",
+32
src/data/guards.ts
··· 26 26 /** Type guard for array values */ 27 27 export const isArray = (x: unknown) => Array.isArray(x) 28 28 29 + /** Type guard for Set values */ 30 + export const isSet = (x: unknown): x is Set<unknown> => x instanceof Set 31 + 32 + /** Type guard for Map values */ 33 + export const isMap = (x: unknown): x is Map<unknown, unknown> => x instanceof Map 34 + 29 35 // ============================================================================= 30 36 // Number Property Guards 31 37 // ============================================================================= ··· 99 105 <K extends string>(...keys: K[]) => 100 106 <T>(x: T): x is T & Record<K, unknown> => 101 107 isObject(x) && keys.every((k) => k in x) 108 + 109 + // ============================================================================= 110 + // Emptiness Predicates 111 + // ============================================================================= 112 + 113 + /** A value whose emptiness can be meaningfully checked */ 114 + type Emptiable = 115 + | string 116 + | readonly unknown[] 117 + | ReadonlySet<unknown> 118 + | ReadonlyMap<unknown, unknown> 119 + | Record<string, unknown> 120 + 121 + /** Returns the number of elements in a string, array, Set, Map, or object */ 122 + export const size = (x: Emptiable): number => 123 + isString(x) || isArray(x) 124 + ? x.length 125 + : isSet(x) || isMap(x) 126 + ? x.size 127 + : Object.keys(x).length 128 + 129 + /** Checks if a string, array, Set, Map, or object is empty */ 130 + export const isEmpty = (x: Emptiable): boolean => size(x) === 0 131 + 132 + /** Checks if a string, array, Set, Map, or object is non-empty */ 133 + export const isNonEmpty = (x: Emptiable): boolean => size(x) > 0
+36
tests/guards.test.ts
··· 3 3 and, 4 4 hasProperties, 5 5 isBoolean, 6 + isEmpty, 7 + isNonEmpty, 6 8 isDefined, 7 9 isNotNull, 8 10 isNotNullish, ··· 69 71 expect(hasIdAndName({})).toBe(false) 70 72 expect(hasIdAndName(null)).toBe(false) 71 73 expect(hasIdAndName("string")).toBe(false) 74 + }) 75 + }) 76 + 77 + describe("emptiness predicates", () => { 78 + it("isEmpty checks strings", () => { 79 + expect(isEmpty("")).toBe(true) 80 + expect(isEmpty("hello")).toBe(false) 81 + }) 82 + 83 + it("isEmpty checks arrays", () => { 84 + expect(isEmpty([])).toBe(true) 85 + expect(isEmpty([1])).toBe(false) 86 + }) 87 + 88 + it("isEmpty checks Sets", () => { 89 + expect(isEmpty(new Set())).toBe(true) 90 + expect(isEmpty(new Set([1]))).toBe(false) 91 + }) 92 + 93 + it("isEmpty checks Maps", () => { 94 + expect(isEmpty(new Map())).toBe(true) 95 + expect(isEmpty(new Map([["a", 1]]))).toBe(false) 96 + }) 97 + 98 + it("isEmpty checks objects", () => { 99 + expect(isEmpty({})).toBe(true) 100 + expect(isEmpty({ a: 1 })).toBe(false) 101 + }) 102 + 103 + it("isNonEmpty is the inverse of isEmpty", () => { 104 + expect(isNonEmpty("")).toBe(false) 105 + expect(isNonEmpty("hi")).toBe(true) 106 + expect(isNonEmpty([])).toBe(false) 107 + expect(isNonEmpty([1])).toBe(true) 72 108 }) 73 109 }) 74 110