feat (v1.0.0): initial refactor and redesign
This commit is contained in:
parent
3058aa1ab4
commit
fe9b50b30e
134 changed files with 17792 additions and 3670 deletions
317
lib/utils/formatting.ts
Normal file
317
lib/utils/formatting.ts
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
/**
|
||||
* Formatter utility class providing consistent formatting functions for various data types.
|
||||
*
|
||||
* @remarks
|
||||
* This class contains static methods for formatting numbers, dates, strings, and other common
|
||||
* data types across the application. All methods are pure functions with no side effects.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { Formatter } from '@/lib/utils'
|
||||
*
|
||||
* // Format currency
|
||||
* const price = Formatter.currency(1234.56) // "$1234.56"
|
||||
*
|
||||
* // Format large numbers
|
||||
* const tokens = Formatter.tokens(1500000) // "1.5M"
|
||||
*
|
||||
* // Format dates
|
||||
* const date = Formatter.date(new Date(), 'long') // "January 15, 2025"
|
||||
* ```
|
||||
*
|
||||
* @category Utils
|
||||
* @public
|
||||
*/
|
||||
export class Formatter {
|
||||
/**
|
||||
* Formats a number as currency with a dollar sign and fixed decimal places.
|
||||
*
|
||||
* @param value - The numeric value to format as currency
|
||||
* @param decimals - Number of decimal places to display (default: 2)
|
||||
* @returns Formatted currency string with dollar sign
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* Formatter.currency(1234.567, 2) // "$1234.57"
|
||||
* Formatter.currency(99.9) // "$99.90"
|
||||
* Formatter.currency(1000, 0) // "$1000"
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
static currency(value: number, decimals: number = 2): string {
|
||||
return `$${value.toFixed(decimals)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats large numbers with metric suffixes (K, M, B) for readability.
|
||||
*
|
||||
* @param value - The numeric value to format
|
||||
* @returns Formatted string with appropriate suffix
|
||||
*
|
||||
* @remarks
|
||||
* - Values >= 1 billion use 'B' suffix
|
||||
* - Values >= 1 million use 'M' suffix
|
||||
* - Values >= 1 thousand use 'K' suffix
|
||||
* - Smaller values return as integers
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* Formatter.tokens(2_500_000_000) // "2.5B"
|
||||
* Formatter.tokens(1_500_000) // "1.5M"
|
||||
* Formatter.tokens(7_500) // "7.5K"
|
||||
* Formatter.tokens(999) // "999"
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
static tokens(value: number): string {
|
||||
if (value >= 1_000_000_000) {
|
||||
return `${(value / 1_000_000_000).toFixed(1)}B`
|
||||
}
|
||||
if (value >= 1_000_000) {
|
||||
return `${(value / 1_000_000).toFixed(1)}M`
|
||||
}
|
||||
if (value >= 1_000) {
|
||||
return `${(value / 1_000).toFixed(1)}K`
|
||||
}
|
||||
return value.toFixed(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a number as a percentage with a percent sign.
|
||||
*
|
||||
* @param value - The numeric value to format as percentage (e.g., 85.5 for 85.5%)
|
||||
* @param decimals - Number of decimal places to display (default: 1)
|
||||
* @returns Formatted percentage string with percent sign
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* Formatter.percentage(85.5) // "85.5%"
|
||||
* Formatter.percentage(100, 0) // "100%"
|
||||
* Formatter.percentage(33.333, 2) // "33.33%"
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
static percentage(value: number, decimals: number = 1): string {
|
||||
return `${value.toFixed(decimals)}%`
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a date object or ISO string into various human-readable formats.
|
||||
*
|
||||
* @param date - Date object or ISO date string to format
|
||||
* @param format - Output format style (default: 'short')
|
||||
* @returns Formatted date string in the specified format
|
||||
*
|
||||
* @remarks
|
||||
* Supported formats:
|
||||
* - 'iso': ISO 8601 format (e.g., "2025-01-15T10:30:00.000Z")
|
||||
* - 'long': Full month name (e.g., "January 15, 2025")
|
||||
* - 'short': Abbreviated month (e.g., "Jan 15, 2025")
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const date = new Date('2025-01-15')
|
||||
* Formatter.date(date, 'short') // "Jan 15, 2025"
|
||||
* Formatter.date(date, 'long') // "January 15, 2025"
|
||||
* Formatter.date(date, 'iso') // "2025-01-15T00:00:00.000Z"
|
||||
*
|
||||
* // Also accepts ISO strings
|
||||
* Formatter.date('2025-01-15', 'short') // "Jan 15, 2025"
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
static date(date: Date | string, format: 'short' | 'long' | 'iso' = 'short'): string {
|
||||
const d = typeof date === 'string' ? new Date(date) : date
|
||||
|
||||
switch (format) {
|
||||
case 'iso':
|
||||
return d.toISOString()
|
||||
case 'long':
|
||||
return d.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})
|
||||
case 'short':
|
||||
default:
|
||||
return d.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a duration in days into a compact human-readable string.
|
||||
*
|
||||
* @param days - Number of days to format
|
||||
* @returns Compact duration string with appropriate unit
|
||||
*
|
||||
* @remarks
|
||||
* Uses the following conversion rules:
|
||||
* - >= 365 days: years ('y')
|
||||
* - >= 30 days: months ('mo')
|
||||
* - >= 7 days: weeks ('w')
|
||||
* - < 7 days: days ('d')
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* Formatter.duration(400) // "1y"
|
||||
* Formatter.duration(45) // "1mo"
|
||||
* Formatter.duration(14) // "2w"
|
||||
* Formatter.duration(5) // "5d"
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
static duration(days: number): string {
|
||||
if (days >= 365) return `${Math.floor(days / 365)}y`
|
||||
if (days >= 30) return `${Math.floor(days / 30)}mo`
|
||||
if (days >= 7) return `${Math.floor(days / 7)}w`
|
||||
return `${days}d`
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a file size in bytes into a human-readable string with appropriate unit.
|
||||
*
|
||||
* @param bytes - File size in bytes
|
||||
* @returns Formatted file size string with unit (B, KB, MB, GB, TB)
|
||||
*
|
||||
* @remarks
|
||||
* - Automatically selects the most appropriate unit based on size
|
||||
* - Uses 1024 as the conversion factor (binary prefix)
|
||||
* - Bytes displayed as integers, larger units with 1 decimal place
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* Formatter.fileSize(512) // "512 B"
|
||||
* Formatter.fileSize(1536) // "1.5 KB"
|
||||
* Formatter.fileSize(1_572_864) // "1.5 MB"
|
||||
* Formatter.fileSize(1_610_612_736) // "1.5 GB"
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
static fileSize(bytes: number): string {
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
let size = bytes
|
||||
let unitIndex = 0
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024
|
||||
unitIndex++
|
||||
}
|
||||
|
||||
return `${size.toFixed(unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a number with optional decimal places and locale-specific formatting.
|
||||
*
|
||||
* @param value - The numeric value to format
|
||||
* @param decimals - Optional number of decimal places (uses locale formatting if omitted)
|
||||
* @returns Formatted number string
|
||||
*
|
||||
* @remarks
|
||||
* - With decimals specified: Uses toFixed() for exact decimal control
|
||||
* - Without decimals: Uses toLocaleString() for thousands separators
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* Formatter.number(1234567) // "1,234,567" (with locale separators)
|
||||
* Formatter.number(1234.567, 2) // "1234.57" (exact decimals)
|
||||
* Formatter.number(100, 0) // "100"
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
static number(value: number, decimals?: number): string {
|
||||
if (decimals !== undefined) {
|
||||
return value.toFixed(decimals)
|
||||
}
|
||||
return value.toLocaleString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Capitalizes the first letter of a string and lowercases the rest.
|
||||
*
|
||||
* @param str - The string to capitalize
|
||||
* @returns String with first letter uppercase and remaining letters lowercase
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* Formatter.capitalize('hello') // "Hello"
|
||||
* Formatter.capitalize('WORLD') // "World"
|
||||
* Formatter.capitalize('hELLo WoRLd') // "Hello world"
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
static capitalize(str: string): string {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase()
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncates a string to a maximum length and adds a suffix if truncated.
|
||||
*
|
||||
* @param str - The string to truncate
|
||||
* @param maxLength - Maximum length including suffix
|
||||
* @param suffix - String to append when truncated (default: '...')
|
||||
* @returns Original string if within limit, otherwise truncated string with suffix
|
||||
*
|
||||
* @remarks
|
||||
* The suffix length is included in maxLength calculation, so the resulting
|
||||
* string will never exceed maxLength characters.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* Formatter.truncate('Hello World', 8) // "Hello..."
|
||||
* Formatter.truncate('Hi', 10) // "Hi"
|
||||
* Formatter.truncate('Long text here', 10, '~') // "Long text~"
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
static truncate(str: string, maxLength: number, suffix: string = '...'): string {
|
||||
if (str.length <= maxLength) return str
|
||||
return str.slice(0, maxLength - suffix.length) + suffix
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string to a URL-friendly slug format.
|
||||
*
|
||||
* @param str - The string to convert to a slug
|
||||
* @returns URL-safe slug string
|
||||
*
|
||||
* @remarks
|
||||
* Transformation steps:
|
||||
* 1. Converts to lowercase
|
||||
* 2. Trims whitespace
|
||||
* 3. Removes non-word characters (except spaces and hyphens)
|
||||
* 4. Replaces spaces, underscores, and multiple hyphens with single hyphen
|
||||
* 5. Removes leading/trailing hyphens
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* Formatter.slugify('Hello World') // "hello-world"
|
||||
* Formatter.slugify('My_Page Title!') // "my-page-title"
|
||||
* Formatter.slugify(' trim spaces ') // "trim-spaces"
|
||||
* Formatter.slugify('foo---bar') // "foo-bar"
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
static slugify(str: string): string {
|
||||
return str
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^\w\s-]/g, '')
|
||||
.replace(/[\s_-]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
}
|
||||
}
|
||||
3
lib/utils/index.ts
Normal file
3
lib/utils/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { Formatter } from './formatting'
|
||||
export { Validator } from './validation'
|
||||
export * from './styles'
|
||||
56
lib/utils/styles.ts
Normal file
56
lib/utils/styles.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* Props to apply to external links for security and UX best practices.
|
||||
*
|
||||
* @remarks
|
||||
* These props should be spread onto anchor tags that link to external sites:
|
||||
* - `target="_blank"`: Opens link in new tab
|
||||
* - `rel="noopener noreferrer"`: Prevents security vulnerabilities and referrer leakage
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* import { externalLinkProps } from '@/lib/utils/styles'
|
||||
*
|
||||
* <a href="https://external-site.com" {...externalLinkProps}>
|
||||
* Visit Site
|
||||
* </a>
|
||||
* ```
|
||||
*
|
||||
* @category Utils
|
||||
* @public
|
||||
*/
|
||||
export const externalLinkProps = {
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer'
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Type guard to check if a URL string is an external link (starts with http/https).
|
||||
*
|
||||
* @param href - URL string to check
|
||||
* @returns Type predicate narrowing href to `http${string}` if external
|
||||
*
|
||||
* @remarks
|
||||
* This function is useful for conditional rendering of external link attributes
|
||||
* or icons. It narrows the TypeScript type to indicate an HTTP(S) URL.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* import { isExternalHref, externalLinkProps } from '@/lib/utils/styles'
|
||||
*
|
||||
* function Link({ href, children }) {
|
||||
* const external = isExternalHref(href)
|
||||
*
|
||||
* return (
|
||||
* <a href={href} {...(external ? externalLinkProps : {})}>
|
||||
* {children}
|
||||
* {external && <ExternalIcon />}
|
||||
* </a>
|
||||
* )
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @category Utils
|
||||
* @public
|
||||
*/
|
||||
export const isExternalHref = (href?: string): href is `http${string}` =>
|
||||
typeof href === 'string' && href.startsWith('http')
|
||||
302
lib/utils/validation.ts
Normal file
302
lib/utils/validation.ts
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
/**
|
||||
* Validator utility class providing type-safe validation functions with TypeScript type guards.
|
||||
*
|
||||
* @remarks
|
||||
* This class contains static methods for validating various data types and formats.
|
||||
* Most methods use TypeScript type predicates for runtime type checking and compile-time
|
||||
* type narrowing.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { Validator } from '@/lib/utils'
|
||||
*
|
||||
* // Validate and narrow types
|
||||
* if (Validator.isValidDate(value)) {
|
||||
* // TypeScript knows value is Date here
|
||||
* console.log(value.getTime())
|
||||
* }
|
||||
*
|
||||
* // Validate strings
|
||||
* const isValid = Validator.isValidEmail('user@example.com') // true
|
||||
*
|
||||
* // Validate ranges
|
||||
* const inRange = Validator.isInRange(50, 0, 100) // true
|
||||
* ```
|
||||
*
|
||||
* @category Utils
|
||||
* @public
|
||||
*/
|
||||
export class Validator {
|
||||
/**
|
||||
* Validates that a value is a valid Date object with a valid timestamp.
|
||||
*
|
||||
* @param date - Value to check
|
||||
* @returns Type predicate indicating if value is a valid Date
|
||||
*
|
||||
* @remarks
|
||||
* Checks both that the value is a Date instance and that its internal
|
||||
* timestamp is not NaN (which can occur with invalid date strings).
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* Validator.isValidDate(new Date()) // true
|
||||
* Validator.isValidDate(new Date('2025-01-15')) // true
|
||||
* Validator.isValidDate(new Date('invalid')) // false
|
||||
* Validator.isValidDate('2025-01-15') // false
|
||||
* Validator.isValidDate(null) // false
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
static isValidDate(date: unknown): date is Date {
|
||||
return date instanceof Date && !isNaN(date.getTime())
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a string is a properly formatted URL.
|
||||
*
|
||||
* @param url - String to validate as URL
|
||||
* @returns True if the string is a valid URL, false otherwise
|
||||
*
|
||||
* @remarks
|
||||
* Uses the built-in URL constructor to validate format. Accepts any protocol
|
||||
* (http, https, ftp, etc.) and properly formatted URLs.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* Validator.isValidUrl('https://example.com') // true
|
||||
* Validator.isValidUrl('http://localhost:3000') // true
|
||||
* Validator.isValidUrl('ftp://files.example.com') // true
|
||||
* Validator.isValidUrl('example.com') // false (no protocol)
|
||||
* Validator.isValidUrl('not a url') // false
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
static isValidUrl(url: string): boolean {
|
||||
try {
|
||||
new URL(url)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a string matches a basic email format.
|
||||
*
|
||||
* @param email - String to validate as email address
|
||||
* @returns True if the string matches email format, false otherwise
|
||||
*
|
||||
* @remarks
|
||||
* Uses a basic regex pattern that checks for:
|
||||
* - Non-whitespace characters before @
|
||||
* - Non-whitespace characters after @
|
||||
* - A dot followed by non-whitespace characters (TLD)
|
||||
*
|
||||
* Note: This is a basic format check and may not catch all invalid emails
|
||||
* or allow all technically valid ones. For production use, consider more
|
||||
* robust validation or server-side verification.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* Validator.isValidEmail('user@example.com') // true
|
||||
* Validator.isValidEmail('test.user@domain.co') // true
|
||||
* Validator.isValidEmail('invalid.email') // false
|
||||
* Validator.isValidEmail('missing@domain') // false
|
||||
* Validator.isValidEmail('@example.com') // false
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
static isValidEmail(email: string): boolean {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
return emailRegex.test(email)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a string matches a valid domain name format.
|
||||
*
|
||||
* @param domain - String to validate as domain name
|
||||
* @returns True if the string is a valid domain format, false otherwise
|
||||
*
|
||||
* @remarks
|
||||
* Validates domain names according to standard rules:
|
||||
* - Only alphanumeric characters and hyphens
|
||||
* - Cannot start or end with hyphen
|
||||
* - Maximum 63 characters per label
|
||||
* - Case insensitive
|
||||
*
|
||||
* Accepts both top-level domains and subdomains.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* Validator.isValidDomain('example.com') // true
|
||||
* Validator.isValidDomain('subdomain.example.com') // true
|
||||
* Validator.isValidDomain('test-site.dev') // true
|
||||
* Validator.isValidDomain('Example.COM') // true (case insensitive)
|
||||
* Validator.isValidDomain('-invalid.com') // false
|
||||
* Validator.isValidDomain('invalid-.com') // false
|
||||
* Validator.isValidDomain('has space.com') // false
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
static isValidDomain(domain: string): boolean {
|
||||
const domainRegex = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i
|
||||
return domainRegex.test(domain)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a number falls within a specified range (inclusive).
|
||||
*
|
||||
* @param value - Number to check
|
||||
* @param min - Minimum allowed value (inclusive)
|
||||
* @param max - Maximum allowed value (inclusive)
|
||||
* @returns True if value is between min and max (inclusive), false otherwise
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* Validator.isInRange(50, 0, 100) // true
|
||||
* Validator.isInRange(0, 0, 100) // true (inclusive)
|
||||
* Validator.isInRange(100, 0, 100) // true (inclusive)
|
||||
* Validator.isInRange(-1, 0, 100) // false
|
||||
* Validator.isInRange(101, 0, 100) // false
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
static isInRange(value: number, min: number, max: number): boolean {
|
||||
return value >= min && value <= max
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that an object contains all required keys.
|
||||
*
|
||||
* @template T - The expected object type
|
||||
* @param obj - Value to check
|
||||
* @param keys - Array of required keys
|
||||
* @returns Type predicate indicating if obj has all required keys
|
||||
*
|
||||
* @remarks
|
||||
* This is a type guard that performs runtime validation while also narrowing
|
||||
* the TypeScript type. It only checks for key presence, not value types.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* interface User {
|
||||
* name: string
|
||||
* email: string
|
||||
* age: number
|
||||
* }
|
||||
*
|
||||
* const data: unknown = { name: 'Alice', email: 'alice@example.com', age: 30 }
|
||||
*
|
||||
* if (Validator.hasRequiredKeys<User>(data, ['name', 'email', 'age'])) {
|
||||
* // TypeScript knows data is User here
|
||||
* console.log(data.name) // OK
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
static hasRequiredKeys<T extends object>(
|
||||
obj: unknown,
|
||||
keys: (keyof T)[]
|
||||
): obj is T {
|
||||
if (typeof obj !== 'object' || obj === null) return false
|
||||
return keys.every(key => key in obj)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a value is a non-empty string (after trimming).
|
||||
*
|
||||
* @param value - Value to check
|
||||
* @returns Type predicate indicating if value is a non-empty string
|
||||
*
|
||||
* @remarks
|
||||
* Trims whitespace before checking length, so strings with only whitespace
|
||||
* are considered empty.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* Validator.isNonEmptyString('hello') // true
|
||||
* Validator.isNonEmptyString(' text ') // true
|
||||
* Validator.isNonEmptyString('') // false
|
||||
* Validator.isNonEmptyString(' ') // false (whitespace only)
|
||||
* Validator.isNonEmptyString(123) // false
|
||||
* Validator.isNonEmptyString(null) // false
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
static isNonEmptyString(value: unknown): value is string {
|
||||
return typeof value === 'string' && value.trim().length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a value is a positive number (> 0) and not NaN.
|
||||
*
|
||||
* @param value - Value to check
|
||||
* @returns Type predicate indicating if value is a positive number
|
||||
*
|
||||
* @remarks
|
||||
* Checks for:
|
||||
* - Type is number
|
||||
* - Value is greater than 0 (not equal to 0)
|
||||
* - Value is not NaN
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* Validator.isPositiveNumber(5) // true
|
||||
* Validator.isPositiveNumber(0.1) // true
|
||||
* Validator.isPositiveNumber(0) // false
|
||||
* Validator.isPositiveNumber(-5) // false
|
||||
* Validator.isPositiveNumber(NaN) // false
|
||||
* Validator.isPositiveNumber('5') // false
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
static isPositiveNumber(value: unknown): value is number {
|
||||
return typeof value === 'number' && value > 0 && !isNaN(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a value is an array, optionally validating each item.
|
||||
*
|
||||
* @template T - The expected item type
|
||||
* @param value - Value to check
|
||||
* @param itemValidator - Optional validator function for array items
|
||||
* @returns Type predicate indicating if value is an array of type T
|
||||
*
|
||||
* @remarks
|
||||
* - Without itemValidator: Only checks if value is an array
|
||||
* - With itemValidator: Checks if value is an array AND all items pass validation
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Basic array check
|
||||
* Validator.isArray([1, 2, 3]) // true
|
||||
* Validator.isArray('not array') // false
|
||||
*
|
||||
* // With item validation
|
||||
* Validator.isArray([1, 2, 3], (item): item is number => typeof item === 'number') // true
|
||||
* Validator.isArray([1, '2', 3], (item): item is number => typeof item === 'number') // false
|
||||
*
|
||||
* // With type narrowing
|
||||
* const value: unknown = ['a', 'b', 'c']
|
||||
* if (Validator.isArray<string>(value, (item): item is string => typeof item === 'string')) {
|
||||
* // TypeScript knows value is string[] here
|
||||
* value.map(s => s.toUpperCase())
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
static isArray<T>(value: unknown, itemValidator?: (item: unknown) => item is T): value is T[] {
|
||||
if (!Array.isArray(value)) return false
|
||||
if (!itemValidator) return true
|
||||
return value.every(itemValidator)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue