Files
kycnotme/web/src/lib/arrays.ts
2025-05-19 10:10:06 +00:00

131 lines
3.5 KiB
TypeScript

import { z } from 'astro/zod'
import { map, xor, some, every } from 'lodash-es'
/**
* Returns a tuple of values from a tuple of records.
*
* @example
* TupleValues<'value', [{ value: 'a' }, { value: 'b' }]> // -> ['a', 'b']
*/
type TupleValues<K extends string, T extends readonly Record<K, string>[]> = {
[I in keyof T]: T[I][K]
}
/**
* Returns a tuple of values from a tuple of records.
*
* @example
* const options = [{ value: 'a' }, { value: 'b' }]
* const values = getTupleValues(options, 'value') // -> ['a', 'b']
*/
export function getTupleValues<K extends string, T extends readonly Record<K, string>[]>(
arr: T,
key: K
): TupleValues<K, T> {
return map(arr, key) as unknown as TupleValues<K, T>
}
/**
* Returns a zod enum from a constant.
*
* @example
* const options = [{ value: 'a' }, { value: 'b' }]
* const zodEnum = zodEnumFromConstant(options, 'value') // -> z.enum(['a', 'b'])
*/
export function zodEnumFromConstant<K extends string, T extends readonly Record<K, string>[]>(
arr: T,
key: K
) {
return z.enum(getTupleValues(arr as unknown as [T[number], ...T[number][]], key))
}
/**
* Returns a random element from an array.
*
* @example
* const options = ['a', 'b']
* const randomElement = getRandomElement(options) // -> 'a' or 'b'
*/
export function getRandom<T>(array: readonly T[]): T {
return array[Math.floor(Math.random() * array.length)] as T
}
/**
* Typed version of Array.prototype.join.
*
* @example
* TypedJoin<['a', 'b']> // -> 'ab'
* TypedJoin<['a', 'b'], '-'> // -> 'a-b'
*/
type TypedJoin<
T extends readonly string[],
Separator extends string = '',
First extends boolean = true,
> = T extends readonly []
? ''
: T extends readonly [infer U extends string, ...infer R extends readonly string[]]
? `${First extends true ? '' : Separator}${U}${TypedJoin<R, Separator, false>}`
: string
/**
* Joins an array of strings with a separator.
*
* @example
* const options = ['a', 'b'] as const
* const joined = typedJoin(options) // -> 'ab'
* const joinedWithSeparator = typedJoin(options, '-') // -> 'a-b'
*/
export function typedJoin<T extends readonly string[], Separator extends string = ''>(
array: T,
separator: Separator = '' as Separator
) {
return array.join(separator) as TypedJoin<T, Separator>
}
/**
* Checks if two or more arrays are equal without considering order.
*
* @example
* const a = [1, 2, 3]
* const b = [3, 2, 1]
* areEqualArraysWithoutOrder(a, b) // -> true
*/
export function areEqualArraysWithoutOrder<T>(...arrays: (T[] | null | undefined)[]): boolean {
return xor(...arrays).length === 0
}
/**
* Checks if some but not all of the arguments are true.
*
* @example
* someButNotAll(true, false, true) // -> true
* someButNotAll(true, true, true) // -> false
* someButNotAll(false, false, false) // -> false
*/
export const someButNotAll = (...args: boolean[]) => some(args) && !every(args)
/**
* Returns undefined if the array is empty.
*
* @example
* const a = [1, 2, 3]
* const b = []
* const c = null
* const d = undefined
* undefinedIfEmpty(a) // -> [1, 2, 3]
* undefinedIfEmpty(b) // -> undefined
* undefinedIfEmpty(c) // -> undefined
* undefinedIfEmpty(d) // -> undefined
*/
export function undefinedIfEmpty<T>(value: T) {
return (value && Array.isArray(value) && value.length > 0 ? value : undefined) as T extends (infer U)[]
? U[] | undefined
: T extends readonly [...infer U]
? U | undefined
: undefined
}
export function isNotArray<T>(value: T | T[]): value is T {
return !Array.isArray(value)
}