diff --git a/eslint.config.ts b/eslint.config.ts index fa7b48a..135d8a5 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -46,6 +46,7 @@ export default defineConfig([ }, ], "@typescript-eslint/no-namespace": "off", + "@typescript-eslint/no-empty-object-type": "off", }, languageOptions: { parserOptions: { diff --git a/package.json b/package.json index 544f94d..28c8c2b 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "jiti": "^2.7.0", "madge": "^8.0.0", "prettier": "^3.8.4", - "typescript": "~6.0.2", + "typescript": "6", "typescript-eslint": "^8.59.2", "vitest": "^4.1.9" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 20d340b..113bbe8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,7 +39,7 @@ importers: specifier: ^3.8.4 version: 3.8.4 typescript: - specifier: ~6.0.2 + specifier: '6' version: 6.0.3 typescript-eslint: specifier: ^8.59.2 diff --git a/src/base/hkt.ts b/src/base/hkt.ts index d5573f0..d3944d1 100644 --- a/src/base/hkt.ts +++ b/src/base/hkt.ts @@ -1,18 +1,19 @@ export interface HKT { - readonly _meta: In; + readonly _in: In; + readonly _out: Out; readonly _t: unknown; new: (t: never) => Out; } type Param< This extends HKT, - U = This["_meta"], + U = This["_in"], > = This["_t"] extends infer T ? (T extends U ? T : U) : never; export namespace HKT { export type T< This extends HKT, - T = This extends { _meta: infer I } ? I : unknown, + T = This["_in"], //This extends { _in: infer I } ? I : unknown, > = Param; export type Apply = ReturnType< (T & { _t: t })["new"] diff --git a/src/base/index.ts b/src/base/index.ts index 9e3cd5c..4e3ccaf 100644 --- a/src/base/index.ts +++ b/src/base/index.ts @@ -33,8 +33,7 @@ export function makeFluent( const f = { value } as unknown as Fluent; for (const mixin of registry) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - mixin.fn(value, f, fluent as (value: unknown) => never); + mixin.fn(value, f, fluent); } return f; diff --git a/src/base/mixin.ts b/src/base/mixin.ts index 13793a1..f3de819 100644 --- a/src/base/mixin.ts +++ b/src/base/mixin.ts @@ -1,6 +1,7 @@ import type { Fluent } from "."; import { never } from "../internal"; import type { Registry } from "../registry"; +import type { RecPartial } from "../utility"; import type { HKT } from "./hkt"; export interface Props { @@ -8,28 +9,43 @@ export interface Props { readonly meta: { registry: Registry }; } -type MixinHKT = HKT; -type MixinFn = ( - value: unknown, - fluent: Record, - callback: (value: unknown) => never, +type MixinHKT = HKT>; +type MixinFn = ( + value: T, + fluent: object & RecPartial, + callback: (value: U) => Fluent, ) => void; -export interface Mixin { +export interface Mixin< + I = unknown, + T extends MixinHKT = MixinHKT, +> { + interface: I; hkt: T; - fn: MixinFn; + fn: MixinFn; } -export function Mixin(fn: MixinFn): Mixin { +export function Mixin>( + fn: MixinFn, +): Mixin { return { + interface: never, hkt: never, fn, }; } export namespace Mixin { - export type HKT = MixinHKT; - export type Function = MixinFn; + export type HKT = MixinHKT; + export function partial( + callback: ( + value: T, + fluent: object & RecPartial, + callback:

(value: P) => Fluent, + ) => void, + ): typeof callback { + return callback; + } } export declare const shim: unique symbol; diff --git a/src/internal.ts b/src/internal.ts index 58585d9..99f91ad 100644 --- a/src/internal.ts +++ b/src/internal.ts @@ -7,6 +7,10 @@ export interface Identity extends HKT { // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion export const never = undefined as never; +export function isArray(v: unknown): v is unknown[] { + return typeof v === "object" && v !== null && Array.isArray(v); +} + export class AssertionError extends Error { public constructor(msg?: string) { super(msg); @@ -26,9 +30,11 @@ export function assert( ); } -// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters, @typescript-eslint/no-unused-vars -export function assertType(_v: unknown): asserts _v is T { - /* empty */ +export namespace assert { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + export function type(_v: unknown): asserts _v is NoInfer { + /* empty */ + } } // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type diff --git a/src/mixin/array.ts b/src/mixin/array.ts index 3153a60..e210e5a 100644 --- a/src/mixin/array.ts +++ b/src/mixin/array.ts @@ -1,7 +1,12 @@ import { makeFluent } from "../base"; import type { HKT } from "../base/hkt"; -import { Mixin, type Input, type Return } from "../base/mixin"; -import { assert, assertType, type HidePrototype } from "../internal"; +import { + Mixin, + type Input, + type Props, + type Return, +} from "../base/mixin"; +import { assert, type HidePrototype } from "../internal"; import type { MaxDepth, At } from "../utility"; type IterArgs = [ @@ -50,338 +55,315 @@ function isArray(v: unknown): v is readonly unknown[] { return typeof v === "object" && globalThis.Array.isArray(v); } -export interface Array extends Mixin.HKT { - new: (t: HKT.T) => Input extends infer T - ? T extends readonly (infer Item)[] +interface IArray< + T extends readonly unknown[] = readonly unknown[], + t extends Props = Props, +> { + /** + * Index the array using the specified zero-based index. + * Negative indexes start at the end of the array. + * @param index Zero-based index + * @from {@link Array `Array`} + * @example + * ```ts + * const array = ["first", "middle", "last"] as const; + * + * const first = $(array).at(0).value; + * const middle = $(array).at(1).value; + * const last = $(array).at(-1).value; + * + * expect(first).toBe("first"); + * expect(middle).toBe("middle"); + * expect(last).toBe("last"); + * + * const oob = $(array).at(10).value; + * expect(oob).toBe(undefined); + * ``` + */ + at: ( + /** Zero-based index */ + index: K, + ) => Return, t>; + /** + * Interospect each item of the array using the specified + * callback + * @param callback Function called with each element of the array + * @from {@link Array `Array`} + * @example + * ```ts + * const array = [4, 5, 6] as const; + * + * let sum = 0; + * const v = $(array).each((n) => { sum += n }).value; + * + * expect(sum).toBe(15); + * expect(v).toMatchObject(array); + * ``` + */ + each: ( + /** + * Predicate to run on every element + * @param element The current element + * @param index The index of the current element in the array + * @param array The array being iterated + */ + callback: (...args: IterArgs) => void, + ) => Return; + /** + * Transform each item of the array using the specified callback + * @param callback Predicate to compute each new value with + * @from {@link Array `Array`} + * @example + * ```ts + * const chars = ["h", "e", "l", "l", "o"] as const; + * + * const v = $(chars).map(ch => ch.toUpperCase()).value; + * expect(v).toMatchObject(["H", "E", "L", "L", "O"]); + * ``` + */ + map: ( + /** + * Predicate to compute each new value of the array + * @param element The current element + * @param index The index of the current element in the array + * @param array The array being iterated + * @return The new value to use for this element + */ + callback: (...args: IterArgs) => U, + ) => Return, t>; + /** + * Extend the array by repeating its current contents n times + * @param count The amount of repetitions to extend the array by + * @from {@link Array `Array`} + * @example + * ```ts + * const array = [1, 2]; + * + * const v = $(array).extend(2).value; + * expect(v).toMatchObject([1, 2, 1, 2, 1, 2]); + * ``` + */ + extend: ( + /** The amount of repetitions to extend the array by */ + count: N, + ) => Return, t>; + /** + * Filter the array to contain only items satisfying the + * specified callback conditional + * @param callback Predicate determining whether each element should be kept in the array + * @from {@link Array `Array`} + * @example + * ```ts + * const dirty = [-2, 4, 1, -5, -6] as const; + * + * const v = $(dirty).filter(v => v >= 0).value; + * expect(v).toMatchObject([4, 1]); + * ``` + */ + filter: (( + /** + * Predicate to compute filtering with + * @param element The current element + * @param index The index of the current element in the array + * @param array The array being iterated + * @return `true` if the value should be kept and `false` if it should not + */ + callback: (...args: IterArgs) => boolean, + ) => Return, t>) & { + /** + * Filter the array to only contain items which are not + * `null` or `undefined`. + * @from {@link Array `Array`} + * @example + * ```ts + * const array = new Array(5); + * + * array[0] = 1; + * array[1] = 5; + * array[2] = 2; + * + * const v = $(array).filter.some().value; + * expect(v).toMatchObject([1, 5, 2]); + * ``` + */ + some: () => Return< + T extends unknown[] + ? Exclude[] + : readonly Exclude[], + t + >; + } & HidePrototype; + /** + * Collapse array into one value using the specified accumulator callback + * @param initial The initial value of the accumulator + * @param callback Predicate used to compute the new value of the accumulator with each element + * @from {@link Array `Array`} + * @example + * ```ts + * const data = [-5, -4, -2, 1, 2, 3, 5] as const; + * + * const sum = $(data) + * .reduce(0, (total, value) => total + value) + * .value; + * expect(sum).toBe(0); + * ``` + */ + reduce: (( + /** + * Initial value to use for the accumulator + */ + initial: U, + /** + * Predicate to compute the new accumulator value + * @param value The current status of the accumulator + * @param element The current element + * @param index The index of the current element in the array + * @param array The array being iterated + */ + callback: (value: U, ...args: IterArgs) => U, + ) => Return) & { + /** + * Collapse the array using the specified accumulator function, starting from the right. + * @param initial The initial value of the accumulator + * @param callback Predicate used to compute the new value of the accumulator with each element + * @see `.reduce()` + * @example + * ```ts + * const path = ["a", "b", "c"] as const; + * + * const v = $(path) + * .reduce.right({}, (obj, key) => ({ [key]: obj })) + * .value; + * expect(v).toMatchObject({ a: { b: { c: { } } } }); + * ``` + */ + right: ( + /** + * Initial value to use for the accumulator + */ + initial: U, + /** + * Predicate to compute the new accumulator value + * @param value The current status of the accumulator + * @param element The current element + * @param index The index of the current element in the array + * @param array The array being iterated + */ + callback: (value: U, ...args: IterArgs) => U, + ) => Return; + } & HidePrototype; + /** + * Get the number of items in the array + * @example + * ```ts + * const apples = ["red", "green", "yellow"]; + * + * const count = $(apples).length().value; + * expect(count).toBe(3); + * ``` + */ + length: () => Return; + + /** + * Rearrange the items in the array in accordance to the + * specified sorting function + * @param sort The sorting algorithm function to determine the correct order of elements + * @see Nested properties for various pre-made sorting algorithms. + * @from {@link Array `Array`} + * @example + * ```ts + * const unordered = [7, 2, 4, 6, 3, 5, 1]; + * + * const ordered = $(unordered) + * .sort((a, b) => a - b) + * .value; + * expect(ordered).toMatchObject([1, 2, 3, 4, 5, 6, 7]); + * ``` + */ + sort: (( + /** + * Predicate used to compare and determine the ordering + * between any 2 elements of the array + * @param a One element of the array to comapare + * @param b Another element of the array to compare against + * @param arr A view of the array being sorted + * @return A negative value if `a` should come before + * `b`, a positive value if `a` should come after `b`, + * and `0` if `a` and `b` are considered the same. + */ + callback: ( + a: T[number], + b: T[number], + arr: Readonly, + ) => number, + ) => Return, t>) & + ([T[number]] extends [string] ? { /** - * Index the array using the specified zero-based index. - * Negative indexes start at the end of the array. - * @param index Zero-based index - * @from {@link Array `Array`} - * @example - * ```ts - * const array = ["first", "middle", "last"] as const; - * - * const first = $(array).at(0).value; - * const middle = $(array).at(1).value; - * const last = $(array).at(-1).value; - * - * expect(first).toBe("first"); - * expect(middle).toBe("middle"); - * expect(last).toBe("last"); - * - * const oob = $(array).at(10).value; - * expect(oob).toBe(undefined); - * ``` + * @param via A callback to compute a + * representation of each element to use for + * sorting */ - at: ( - /** Zero-based index */ - index: K, - ) => Return, typeof t>; - /** - * Interospect each item of the array using the specified - * callback - * @param callback Function called with each element of the array - * @from {@link Array `Array`} - * @example - * ```ts - * const array = [4, 5, 6] as const; - * - * let sum = 0; - * const v = $(array).each((n) => { sum += n }).value; - * - * expect(sum).toBe(15); - * expect(v).toMatchObject(array); - * ``` - */ - each: ( + alpha: ( /** - * Predicate to run on every element - * @param element The current element - * @param index The index of the current element in the array - * @param array The array being iterated + * @param v The current element + * @return A string representation of `v` */ - callback: (...args: IterArgs) => void, - ) => Return; - /** - * Transform each item of the array using the specified callback - * @param callback Predicate to compute each new value with - * @from {@link Array `Array`} - * @example - * ```ts - * const chars = ["h", "e", "l", "l", "o"] as const; - * - * const v = $(chars).map(ch => ch.toUpperCase()).value; - * expect(v).toMatchObject(["H", "E", "L", "L", "O"]); - * ``` - */ - map: ( - /** - * Predicate to compute each new value of the array - * @param element The current element - * @param index The index of the current element in the array - * @param array The array being iterated - * @return The new value to use for this element - */ - callback: (...args: IterArgs) => U, - ) => Return, typeof t>; - /** - * Extend the array by repeating its current contents n times - * @param count The amount of repetitions to extend the array by - * @from {@link Array `Array`} - * @example - * ```ts - * const array = [1, 2]; - * - * const v = $(array).extend(2).value; - * expect(v).toMatchObject([1, 2, 1, 2, 1, 2]); - * ``` - */ - extend: ( - /** The amount of repetitions to extend the array by */ - count: N, - ) => Return, typeof t>; - /** - * Filter the array to contain only items satisfying the - * specified callback conditional - * @param callback Predicate determining whether each element should be kept in the array - * @from {@link Array `Array`} - * @example - * ```ts - * const dirty = [-2, 4, 1, -5, -6] as const; - * - * const v = $(dirty).filter(v => v >= 0).value; - * expect(v).toMatchObject([4, 1]); - * ``` - */ - filter: (( - /** - * Predicate to compute filtering with - * @param element The current element - * @param index The index of the current element in the array - * @param array The array being iterated - * @return `true` if the value should be kept and `false` if it should not - */ - callback: (...args: IterArgs) => boolean, - ) => Return, typeof t>) & { - /** - * Filter the array to only contain items which are not - * `null` or `undefined`. - * @from {@link Array `Array`} - * @example - * ```ts - * const array = new Array(5); - * - * array[0] = 1; - * array[1] = 5; - * array[2] = 2; - * - * const v = $(array).filter.some().value; - * expect(v).toMatchObject([1, 5, 2]); - * ``` - */ - some: () => Return< - T extends unknown[] - ? Exclude< - T[number], - null | undefined - >[] - : readonly Exclude< - T[number], - null | undefined - >[], - typeof t - >; - } & HidePrototype; - /** - * Collapse array into one value using the specified accumulator callback - * @param initial The initial value of the accumulator - * @param callback Predicate used to compute the new value of the accumulator with each element - * @from {@link Array `Array`} - * @example - * ```ts - * const data = [-5, -4, -2, 1, 2, 3, 5] as const; - * - * const sum = $(data) - * .reduce(0, (total, value) => total + value) - * .value; - * expect(sum).toBe(0); - * ``` - */ - reduce: (( - /** - * Initial value to use for the accumulator - */ - initial: U, - /** - * Predicate to compute the new accumulator value - * @param value The current status of the accumulator - * @param element The current element - * @param index The index of the current element in the array - * @param array The array being iterated - */ - callback: ( - value: U, - ...args: IterArgs - ) => U, - ) => Return) & { - /** - * Collapse the array using the specified accumulator function, starting from the right. - * @param initial The initial value of the accumulator - * @param callback Predicate used to compute the new value of the accumulator with each element - * @see `.reduce()` - * @example - * ```ts - * const path = ["a", "b", "c"] as const; - * - * const v = $(path) - * .reduce.right({}, (obj, key) => ({ [key]: obj })) - * .value; - * expect(v).toMatchObject({ a: { b: { c: { } } } }); - * ``` - */ - right: ( - /** - * Initial value to use for the accumulator - */ - initial: U, - /** - * Predicate to compute the new accumulator value - * @param value The current status of the accumulator - * @param element The current element - * @param index The index of the current element in the array - * @param array The array being iterated - */ - callback: ( - value: U, - ...args: IterArgs - ) => U, - ) => Return; - } & HidePrototype; - /** - * Get the number of items in the array - * @example - * ```ts - * const apples = ["red", "green", "yellow"]; - * - * const count = $(apples).length().value; - * expect(count).toBe(3); - * ``` - */ - length: () => Return; - - /** - * Rearrange the items in the array in accordance to the - * specified sorting function - * @param sort The sorting algorithm function to determine the correct order of elements - * @see Nested properties for various pre-made sorting algorithms. - * @from {@link Array `Array`} - * @example - * ```ts - * const unordered = [7, 2, 4, 6, 3, 5, 1]; - * - * const ordered = $(unordered) - * .sort((a, b) => a - b) - * .value; - * expect(ordered).toMatchObject([1, 2, 3, 4, 5, 6, 7]); - * ``` - */ - sort: (( - /** - * Predicate used to compare and determine the ordering - * between any 2 elements of the array - * @param a One element of the array to comapare - * @param b Another element of the array to compare against - * @param arr A view of the array being sorted - * @return A negative value if `a` should come before - * `b`, a positive value if `a` should come after `b`, - * and `0` if `a` and `b` are considered the same. - */ - callback: ( - a: Item, - b: Item, - arr: Readonly, - ) => number, - ) => Return, typeof t>) & - ([Item] extends [string] - ? { - /** - * @param via A callback to compute a - * representation of each element to use for - * sorting - */ - alpha: ( - /** - * @param v The current element - * @return A string representation of `v` - */ - via?: (v: Item) => string, - ) => Return< - Unordered, - typeof t - >; - } - : { - /** - * @param via A callback to map each element - * of the array to a string that is used for - * sorting - */ - alpha: ( - /** - * @param v The current element - * @return The string representation of `v` - */ - via: (v: Item) => string, - ) => Return< - Unordered, - typeof t - >; - }) & - ([Item] extends [number] - ? { - ascending: () => Return< - Unordered, - typeof t - >; - descending: () => Return< - Unordered, - typeof t - >; - } - : { - ascending: ( - via: (v: Item) => number, - ) => Return< - Unordered, - typeof t - >; - descending: ( - via: (v: Item) => number, - ) => Return< - Unordered, - typeof t - >; - }) & { - /** - * Sort alphabetically (A-Z) by Unicode code points - * @example - * const names = ["Johnny", "Anna", "Bart", "Xavier", "Java"]; - * - * const sorted = $(names).sort.alpha().value; - * expect(sorted).toMatchObject([ - * "Anna", "Bart", "Java", "Johnny", "Xavier" - * ]) - */ - alpha: unknown; - } & HidePrototype; - reverse: () => Return, typeof t>; + via?: (v: T[number]) => string, + ) => Return, t>; } - : unknown + : { + /** + * @param via A callback to map each element + * of the array to a string that is used for + * sorting + */ + alpha: ( + /** + * @param v The current element + * @return The string representation of `v` + */ + via: (v: T[number]) => string, + ) => Return, t>; + }) & + ([T[number]] extends [number] + ? { + ascending: () => Return, t>; + descending: () => Return, t>; + } + : { + ascending: ( + via: (v: T[number]) => number, + ) => Return, t>; + descending: ( + via: (v: T[number]) => number, + ) => Return, t>; + }) & { + /** + * Sort alphabetically (A-Z) by Unicode code points + * @example + * const names = ["Johnny", "Anna", "Bart", "Xavier", "Java"]; + * + * const sorted = $(names).sort.alpha().value; + * expect(sorted).toMatchObject([ + * "Anna", "Bart", "Java", "Johnny", "Xavier" + * ]) + */ + alpha: unknown; + } & HidePrototype; + reverse: () => Return, t>; +} + +export interface Array extends Mixin.HKT { + new: ( + t: HKT.T, + ) => Input extends infer T + ? T extends readonly unknown[] + ? IArray + : {} : never; } -export const Array = Mixin((value, $, fluent) => { +export const Array = Mixin((value, $, fluent) => { if (!isArray(value)) return; $.at = (index: number) => { @@ -407,78 +389,92 @@ export const Array = Mixin((value, $, fluent) => { return fluent(value.map(callback)); }; - $.filter = (callback: (...args: IterArgs) => boolean) => { - return fluent(value.filter(callback)); - }; - assertType($.filter); - Object.assign($.filter, { - some: () => { - return fluent( - value.filter((v) => v !== null && v !== undefined), - ); + $.filter = Object.assign( + (callback: (...args: IterArgs) => boolean) => { + return fluent(value.filter(callback)); }, - }); + { + some: () => { + return fluent( + value.filter( + (v) => v !== null && v !== undefined, + ), + ); + }, + }, + ); - $.reduce = ( - initial: unknown, - callback: (value: unknown, ...args: IterArgs) => unknown, - ) => { - return fluent(value.reduce(callback, initial)); - }; - assertType($.reduce); - Object.assign($.reduce, { - right: ( + $.reduce = Object.assign( + ( initial: unknown, callback: (value: unknown, ...args: IterArgs) => unknown, ) => { - return fluent(value.reduceRight(callback, initial)); + return fluent(value.reduce(callback, initial)); }, - }); + { + right: ( + initial: unknown, + callback: ( + value: unknown, + ...args: IterArgs + ) => unknown, + ) => { + return fluent(value.reduceRight(callback, initial)); + }, + }, + ); $.length = () => { return fluent(value.length); }; - $.sort = ( - callback: ( - a: unknown, - b: unknown, - arr: readonly unknown[], - ) => number, - ) => { - return fluent( - value.toSorted((a, b) => callback(a, b, value)), - ); - }; - assertType($.sort); - Object.assign($.sort, { - alpha: ( - via: (v: unknown) => string = (v) => ( - assert(typeof v === "string"), - v - ), + $.sort = Object.assign( + ( + callback: ( + a: unknown, + b: unknown, + arr: readonly unknown[], + ) => number, ) => { return fluent( - value.toSorted((a, b) => (via(a) < via(b) ? -1 : 1)), + value.toSorted((a, b) => callback(a, b, value)), ); }, - ascending: ( - via: (v: unknown) => number = (v) => ( - assert(typeof v === "number"), - v - ), - ) => { - return fluent(value.toSorted((a, b) => via(a) - via(b))); + { + alpha: ( + via: (v: unknown) => string = (v) => ( + assert(typeof v === "string"), + v + ), + ) => { + return fluent( + value.toSorted((a, b) => + via(a) < via(b) ? -1 : 1, + ), + ); + }, + ascending: ( + via: (v: unknown) => number = (v) => ( + assert(typeof v === "number"), + v + ), + ) => { + return fluent( + value.toSorted((a, b) => via(a) - via(b)), + ); + }, + descending: ( + via: (v: unknown) => number = (v) => ( + assert(typeof v === "number"), + v + ), + ) => { + return fluent( + value.toSorted((a, b) => via(b) - via(a)), + ); + }, }, - descending: ( - via: (v: unknown) => number = (v) => ( - assert(typeof v === "number"), - v - ), - ) => { - return fluent(value.toSorted((a, b) => via(b) - via(a))); - }, - }); + ); $.reverse = () => { return fluent(value.toReversed()); diff --git a/src/mixin/awaited.ts b/src/mixin/awaited.ts index e568a64..6f4be8f 100644 --- a/src/mixin/awaited.ts +++ b/src/mixin/awaited.ts @@ -1,7 +1,13 @@ import { makeFluent } from "../base"; import type { HKT } from "../base/hkt"; -import { Mixin, shim, type Input, type Return } from "../base/mixin"; -import { assert, assertType, never } from "../internal"; +import { + Mixin, + shim, + type Input, + type Props, + type Return, +} from "../base/mixin"; +import { assert, never } from "../internal"; import { Base } from "./base"; interface AwaitedIdentitity extends HKT { @@ -23,20 +29,23 @@ class Awaited> { } } -export interface AsyncMixin extends Mixin.HKT { - new: (t: HKT.T) => Input extends infer T +interface IAsync< + T extends Promise = Promise, + t extends Props = Props, +> { + readonly awaited: Return, t>; + then: ( + fn: (value: T extends Promise ? O : never) => U, + ) => Return, t>; +} + +export interface AsyncMixin extends Mixin.HKT { + new: ( + t: HKT.T, + ) => Input extends infer T ? T extends Promise - ? { - readonly awaited: Return, typeof t>; - then: ( - callback: ( - value: T extends Promise - ? T - : never, - ) => U, - ) => Return, typeof t>; - } - : unknown + ? IAsync + : {} : never; } @@ -45,62 +54,64 @@ interface BaseFluent { [K: PropertyKey]: unknown; } -export const AsyncMixin = Mixin((value, $, fluent) => { - if (!(value instanceof Promise)) return; +export const AsyncMixin = Mixin( + (value, $, fluent) => { + if (!(value instanceof Promise)) return; - $.then = (callback: (value: unknown) => unknown) => { - return fluent(value.then(callback)); - }; + $.then = (callback: (value: unknown) => unknown) => { + return fluent(value.then(callback)); + }; - Object.defineProperty($, "awaited", { - enumerable: true, - get() { - let v: Promise> = value.then((v) => - fluent(v), - ); + Object.defineProperty($, "awaited", { + enumerable: true, + get() { + let v: Promise> = value.then( + (v) => fluent(v), + ); - const path: PropertyKey[] = []; - // eslint-disable-next-line no-empty-pattern - const proxy = new Proxy((...[]: unknown[]) => proxy, { - get: (_, key) => { - if (key === "value") - return v.then((v) => v.value); - path.push(key); - return proxy; - }, - apply: (target, thisArg, args: unknown[]) => { - v = v.then((v) => { - let obj: unknown = v; - for (const node of path) { - assert( - typeof obj === "object" && - obj !== null && - node in obj, - ); - assertType>( - obj, - ); + const path: PropertyKey[] = []; + // eslint-disable-next-line no-empty-pattern + const proxy = new Proxy((...[]: unknown[]) => proxy, { + get: (_, key) => { + if (key === "value") + return v.then((v) => v.value); + path.push(key); + return proxy; + }, + apply: (target, thisArg, args: unknown[]) => { + v = v.then((v) => { + let obj: unknown = v; + for (const node of path) { + assert( + typeof obj === "object" && + obj !== null && + node in obj, + ); + assert.type< + Record + >(obj); - obj = obj[node]; - } + obj = obj[node]; + } - assert(typeof obj === "function"); - assertType< - ( - ...args: unknown[] - ) => BaseFluent - >(obj); + assert(typeof obj === "function"); + assert.type< + ( + ...args: unknown[] + ) => BaseFluent + >(obj); - return obj(...args); - }); - return target.apply(thisArg, args); - }, - }); + return obj(...args); + }); + return target.apply(thisArg, args); + }, + }); - return proxy; - }, - }); -}); + return proxy; + }, + }); + }, +); if (import.meta.vitest) { const { test, expect } = import.meta.vitest; diff --git a/src/mixin/base.ts b/src/mixin/base.ts index 75c8ad0..5227180 100644 --- a/src/mixin/base.ts +++ b/src/mixin/base.ts @@ -1,50 +1,71 @@ import { makeFluent } from "../base"; import type { HKT } from "../base/hkt"; -import { Mixin, type Input, type Return } from "../base/mixin"; +import { + Mixin, + type Input, + type Props, + type Return, +} from "../base/mixin"; -export interface Base extends Mixin.HKT { - new: (t: HKT.T) => Input extends infer T - ? { - /** - * Interospect value using the specified `callback` without - * modifying the value. - * @param callback The interospective callback - * @see `transform` to modify - * @from {@link Base `Base`} - * @example - * ```ts - * let x; - * const value = $(10).tap(v => { x = ++v }).value; - * - * expect(x).toBe(11); - * expect(value).toBe(10); - * ``` - */ - tap( - callback: (value: Readonly) => void, - ): Return; - /** - * Put value through or pipe value through the specified - * `callback` using the outputted return value as a new value. - * A.K.A., _transform_ the current value using a callback - * @param callback The transformative callback - * @from {@link Base `Base`} - * @example - * ```ts - * const value = $("Hello") - * .transform(v => v.toUpperCase()) - * .value; - * expect(value).toBe("HELLO"); - * ``` - */ - transform( - callback: (value: T) => U, - ): Return; - } - : never; +interface IBase { + /** + * Interospect value using the specified `callback` without + * modifying the value. + * @param callback The interospective callback + * @see `transform` to modify + * @from {@link Base `Base`} + * @example + * ```ts + * let x; + * const value = $(10).tap(v => { x = ++v }).value; + * + * expect(x).toBe(11); + * expect(value).toBe(10); + * ``` + */ + tap: (callback: (value: Readonly) => void) => Return; + /** + * Put value through or pipe value through the specified + * `callback` using the outputted return value as a new value. + * A.K.A., _transform_ the current value using a callback + * @param callback The transformative callback + * @from {@link Base `Base`} + * @example + * ```ts + * const value = $("Hello") + * .transform(v => v.toUpperCase()) + * .value; + * expect(value).toBe("HELLO"); + * ``` + */ + transform: (callback: (value: T) => U) => Return; + /** + * Set value to a fallback if the specified callback returns `false` + * @param callback Guarding callback returning `true` to keep the value + * @param fallback The fallback value, `null` by default + * @from {@link Base `Base`} + * @example + * const isObject = (value: unknown) => typeof value === 'object'; + * + * const a = $({ version: '1.0.0' }).where(isObject).value; + * const b = $(42).where(isObject).value; + * const c = $(42).where(isObject, { version: '0.1.0' }).value; + * + * expect(a).toMatchObject({ version: '1.0.0' }); + * expect(b).toBe(null); + * expect(c).toMatchObject({ version: '0.1.0' }); + */ + where: ( + callback: (v: T) => boolean, + fallback?: U, + ) => Return; } -export const Base = Mixin((value, $, fluent) => { +export interface Base extends Mixin.HKT { + new: (t: HKT.T) => IBase, typeof t>; +} + +export const Base = Mixin((value, $, fluent) => { $.tap = (callback: (value: unknown) => void) => { callback(value); return fluent(value); @@ -53,6 +74,11 @@ export const Base = Mixin((value, $, fluent) => { $.transform = (callback: (value: unknown) => unknown) => { return fluent(callback(value)); }; + + $.where = ( + callback: (value: unknown) => boolean, + fallback: unknown = null, + ) => fluent(callback(value) ? value : fallback); }); if (import.meta.vitest) { @@ -83,4 +109,16 @@ if (import.meta.vitest) { $(value).transform(increment).transform(increment).value, ).toBe(increment(increment(value))); }); + + test("where()", () => { + const even = 4 as const; + const odd = 7 as const; + + const isEven = (v: number) => v % 2 === 0; + + expect($(even).where(isEven).value).toBe(even); + expect($(odd).where(isEven).value).toBe(null); + + expect($(odd).where(isEven, -1).value).toBe(-1); + }); } diff --git a/src/mixin/math.ts b/src/mixin/math.ts index d10ad57..92f9e28 100644 --- a/src/mixin/math.ts +++ b/src/mixin/math.ts @@ -1,725 +1,42 @@ -import { makeFluent } from "../base"; import type { HKT } from "../base/hkt"; +import { Mixin, type Input } from "../base/mixin"; +import { isArray } from "../internal"; +import { applyList, type IList } from "./math/list"; +import { applyNumber, type INumber } from "./math/number"; import { - Mixin, - type Input, - type Props, - type Return, -} from "../base/mixin"; -import { assertType } from "../internal"; -import type { Inc } from "../utility"; -import { applySwizzle, type Swizzle } from "./math/swizzle"; + applySwizzle, + type ISwizzle, + type Swizzle, +} from "./math/swizzle"; -type Sign = T extends 0 - ? 0 - : `${T}` extends `-${number}` - ? -1 - : 1; - -type Negate = T extends 0 - ? 0 - : `${T}` extends `-${infer N extends number}` - ? N - : `-${T}` extends `${infer N extends number}` - ? N - : number; - -type Floor = - `${T}` extends `${infer N extends number}.${number}` ? N : T; -type Ceil = - `${T}` extends `${infer N extends number}.${number}` ? Inc : T; -type Round = - `${T}` extends `${infer N extends number}.${0 | 1 | 2 | 3 | 4}${number | ""}` - ? N - : Ceil; - -export interface Math extends Mixin.HKT { - new: (t: HKT.T) => Input extends infer T +interface IMath extends INumber {} +export interface Math extends Mixin.HKT { + new: ( + t: HKT.T, + ) => Input extends infer T ? T extends number - ? { - /** - * `x + y` - * @param other The second term of the addition (`y`) - * @from {@link Math `Math`} - * @example - * ```ts - * const v = $(12).add(8).value; - * expect(v).toBe(20); - * ``` - */ - add: ( - /** Term to add */ - other: number, - ) => Return; - /** - * `x - y` - * @param other The second term of the subtraction (`y`) - * @from {@link Math `Math`} - * @example - * ```ts - * const v = $(12).subtract(8).value; - * expect(v).toBe(4); - * ``` - */ - subtract: ( - /** Term to subtract by */ - other: number, - ) => Return; - /** - * `x * y` - * @param factor Factor term of the multiplication (`y`) - * @from {@link Math `Math`} - * @example - * ```ts - * const v = $(12).multiply(8).value; - * expect(v).toBe(96) - * ``` - */ - multiply: ( - /** Factor to multiply by */ - factor: number, - ) => Return; - /** - * `x ** y` - * @param exponent Exponent term of the power (`y`) - * @from {@link Math `Math`} - * @example - * ```ts - * const v = $(12).pow(2).value; - * expect(v).toBe(144); - * ``` - */ - pow: ( - /** Exponent to raise to */ - exponent: number, - ) => Return; - /** - * `Math.sqrt(x)` - * @from {@link Math `Math`} - * @example - * ```ts - * const v = $(144).sqrt().value; - * expect(v).toBe(12); - * ``` - */ - sqrt: () => Return; - /** - * `x / y` - * @param divisor Divisor term of the division (`y`) - * @from {@link Math `Math`} - * @example - * ```ts - * const v = $(12).divide(8).value; - * expect(v).toBe(1.5); - * ``` - */ - divide: ( - /** Divisor to divide by */ - divisor: number, - ) => Return; - /** - * `x % y` - * @param divisor Divisor term of the remainder operation (`y`) - * @from {@link Math `Math`} - * @example - * ```ts - * const v = $(12).mod(8).value; - * expect(v).toBe(4); - * ``` - */ - mod: ( - /** Divisor to divide by */ - divisor: number, - ) => Return; - /** - * `ln(x)`, `log(x, base)`, or alternatively `Math.log(x) / Math.log(base)` - * @param base Base of the logarithm (`base`). Defaults to `Math.E` for a natural logarithm function - * @from {@link Math `Math`} - * @example - * ```ts - * const a = $(Math.E).log().value; - * expect(a).toBe(1); - * - * const b = $(125).log(5).value; - * expect(b).toBeCloseTo(3); - * ``` - */ - log: ( - /** The base of the logarithm */ - base?: number, - ) => Return; - - /** - * Loosely compare value to the specified other value, - * accounting for floating-point inaccuracies. Comparision - * is performed within some degree of error. - * @param other Value to compare against - * @param delta The degree of acceptable error (defaults to `Number.EPSILON`) - * @from {@link Math `Math`} - * @example - * ```ts - * const result = 0.1 + 0.2; - * const target = 0.3; - * - * const a = $(result).comparesTo(target).value; - * expect(a).toBe(true); - * - * const b = $(result).comparesTo(1, 1).value; - * expect(b).toBe(true); - * - * const c = $(result).comparesTo(0).value; - * expect(c).toBe(false); - * ``` - */ - comparesTo: ( - /** Value to compare against */ - other: number, - /** Degree of acceptable error */ - delta?: number, - ) => Return; - - /** - * `Math.floor(x)` - * @from {@link Math `Math`} - * @example - * ```ts - * const v = $(1.7).floor().value; - * expect(v).toBe(1); - * ``` - */ - floor: () => Return, typeof t>; - /** - * `Math.ceil(x)` - * @from {@link Math `Math`} - * @example - * ```ts - * const v = $(1.3).ceil().value; - * expect(v).toBe(2); - * ``` - */ - ceil: () => Return, typeof t>; - /** - * `Math.round(x)` - * @see `.snapped()` for a rounding with customizable precision - * @from {@link Math `Math`} - * @example - * ```ts - * const a = $(1.3).round().value; - * const b = $(1.7).round().value; - * - * expect(a).toBe(1); - * expect(b).toBe(2); - * ``` - */ - round: () => Return, typeof t>; - /** - * `Math.fround(x)`, round to the nearest 32-bit float - * approximation of value - * @from {@link Math `Math`} - * @example - * ```ts - * const v = $(0.99999999).fround().value; - * expect(v).toBe(1); - * ``` - */ - fround: () => Return; - - /** - * `Math.abs(x)`, calculate the absolute value (distance - * from 0). - * @from {@link Math `Math`} - * @example - * ```ts - * const v = 4 as const; - * - * const a = $(v).abs().value; - * const b = $(-v).abs().value; - * - * expect(a).toBe(v); - * expect(b).toBe(v); - * ``` - */ - abs: () => Return< - `${T}` extends `-${infer N extends number}` - ? N - : T, - typeof t - >; - /** - * `-x` or alternatively `x * -1` - * @from {@link Math `Math`} - * @example - * ```ts - * const v = 10 as const; - * - * const a = $(v).negate().value; - * expect(a).toBe(-v); - * - * const b = $(-v).negate().value; - * expect(b).toBe(v); - * ``` - */ - negate: () => Return, typeof t>; - /** - * `Math.sign(x)`, returns the sign of the value or zero - * @from {@link Math `Math`} - * @example - * ```ts - * const a = $(-42).sign().value; - * const b = $(0).sign().value; - * const c = $(99).sign().value; - * - * expect(a).toBe(-1); - * expect(b).toBe(0); - * expect(c).toBe(1); - * ``` - */ - sign: () => Return, typeof t>; - - /** - * `Math.min(x, ...values)`, pick the _smallest_ out of the - * provided numbers - * @param others Other numbers - * @from {@link Math `Math`} - * @example - * ```ts - * const maximum = 10; - * - * const a = $(5).min(maximum).value; - * const b = $(15).min(maximum).value; - * - * expect(a).toBe(5); - * expect(b).toBe(10); - * ``` - */ - min: ( - /** Other numbers */ - ...others: U - ) => Return; - /** - * `Math.max(x, ...values)`, pick the _largest_ out of the - * provided numbers - * @param others Other numbers - * @from {@link Math `Math`} - * @example - * ```ts - * const minimum = 0; - * - * const a = $(5).max(minimum).value; - * const b = $(-5).max(minimum).value; - * - * expect(a).toBe(5); - * expect(b).toBe(0); - * ``` - */ - max: ( - /** Other numbers */ - ...others: U - ) => Return; - /** - * Clamp value to be within the specified range, or - * alternatively the form of `Math.min(Math.max(x, min), - * max)`. - * @param min Start of the range, the smallest that the value can be - * @param max End of the range, the largest that the value can be - * @from {@link Math `Math`} - * @example - * ```ts - * const min = 0; - * const max = 10; - * - * const a = $(5).clamp(min, max).value; - * const b = $(-2).clamp(min, max).value; - * const c = $(15).clamp(min, max).value; - * - * expect(a).toBe(5); - * expect(b).toBe(0); - * expect(c).toBe(10); - * ``` - */ - clamp: ( - /** The minimum (smallest) value of the range */ - min: number, - /** The maximum (largest) value of the range */ - max: number, - ) => Return; - /** - * Snap or round value to a nearest multiple of the - * specified unit - * @param multiple The unit to snap to - * @from {@link Math `Math`} - * @example - * ```ts - * const unit = 32; - * - * const a = $(32).snapped(unit).value; - * const b = $(48).snapped(unit).value; - * const c = $(64).snapped(unit).value; - * - * expect(a).toBe(32); - * expect(b).toBe(64); - * expect(c).toBe(64); - * ``` - */ - snapped: ( - /** Unit to snap to */ - multiple: number, - ) => Return; - - /** - * Calculate the linear interpolation of value (`a`) to the - * specified target value (`b`) at the specified point - * (`t`). - * - * Alternative forms: `lerp(a, b, t)` or `a * (1 - t) + - * b * t` - * @param b Target value to interpolate to - * @param t Interpolation factor, 0 is `a` and 1 is `b` - * @from {@link Math `Math`} - * @example - * ```ts - * const from = 12; - * const to = 8; - * - * const a = $(from).lerp(to, 0).value; - * const b = $(from).lerp(to, 0.5).value; - * const c = $(from).lerp(to, 1).value; - * - * expect(a).toBe(12); - * expect(b).toBe(10); - * expect(c).toBe(8); - * - * const d = $(from).lerp(to, -1).value; - * const e = $(from).lerp(to, 2).value; - * - * expect(d).toBe(16); - * expect(e).toBe(4); - * ``` - */ - lerp: typeof t extends infer TProps extends Props - ? ( - /** Target value */ - b: number, - /** Interpolation factor, 0 is representative of `a` and 1 is representative of `b` */ - t: number, - ) => Return - : never; - - /** - * `Math.sin(x)` - * @from {@link Math `Math`} - */ - sin: () => Return; - /** - * `Math.asin(x)` - * @from {@link Math `Math`} - */ - asin: () => Return; - /** - * `Math.cos(x)` - * @from {@link Math `Math`} - */ - cos: () => Return; - /** - * `Math.acos(x)` - * @from {@link Math `Math`} - */ - acos: () => Return; - /** - * `Math.tan(x)` - * @from {@link Math `Math`} - */ - tan: () => Return; - /** - * `Math.atan(x)` - * @from {@link Math `Math`} - */ - atan: () => Return; - /** - * `Math.atan2(x, y)` - * @from {@link Math `Math`} - */ - atan2: (y: number) => Return; - - /** - * `Math.sinh(x)` - * @from {@link Math `Math`} - */ - sinh: () => Return; - /** - * `Math.asinh(x)` - * @from {@link Math `Math`} - */ - asinh: () => Return; - /** - * `Math.cosh(x)` - * @from {@link Math `Math`} - */ - cosh: () => Return; - /** - * `Math.acosh(x)` - * @from {@link Math `Math`} - */ - acosh: () => Return; - /** - * `Math.tanh(x)` - * @from {@link Math `Math`} - */ - tanh: () => Return; - /** - * `Math.atanh(x)` - * @from {@link Math `Math`} - */ - atanh: () => Return; - - to: { - /** - * Convert angle to representation in radians (`1 * - * Math.PI` radians per `1 * 180` degrees) - * @from {@link Math `Math`} - * @example - * ```ts - * const v = $(360).to.radians().value; - * expect(v).toBe(2 * Math.PI); - * ``` - */ - radians: () => Return; - /** - * Convert angle to representation in degrees (`1 * 180` - * degrees per `1 * Math.PI` radians) - * @from {@link Math `Math`} - * @example - * ```ts - * const v = $(2 * Math.PI).to.degrees().value; - * expect(v).toBe(360); - * ``` - */ - degrees: () => Return; - }; - } + ? INumber : T extends readonly number[] - ? Swizzle & { - sum: () => Return; - } - : unknown + ? Swizzle & + Omit< + IList, + number extends T["length"] + ? "lerp" | "inverseLerp" + : "" + > + : {} : never; } -const M = globalThis.Math; - -export const Math = Mixin((value, $, fluent) => { - if (typeof value === "number") { - $.add = (other: number) => fluent(value + other); - $.subtract = (other: number) => fluent(value - other); - $.multiply = (factor: number) => fluent(value * factor); - $.pow = (exponent: number) => fluent(M.pow(value, exponent)); - $.sqrt = () => fluent(M.sqrt(value)); - $.divide = (divisor: number) => fluent(value / divisor); - $.mod = (divisor: number) => fluent(value % divisor); - $.log = (base?: number) => { - if (base !== undefined) - return fluent(M.log(value) / M.log(base)); - return fluent(M.log(value)); - }; - - $.comparesTo = ( - other: number, - delta: number = Number.EPSILON, - ) => { - return fluent(M.abs(value - other) < delta); - }; - - $.floor = () => fluent(M.floor(value)); - $.ceil = () => fluent(M.ceil(value)); - $.round = () => fluent(M.round(value)); - $.fround = () => fluent(M.fround(value)); - - $.abs = () => fluent(M.abs(value)); - $.negate = () => fluent(-value); - $.sign = () => fluent(M.sign(value)); - - $.min = (...others: number[]) => - fluent(M.min(value, ...others)); - $.max = (...others: number[]) => - fluent(M.max(value, ...others)); - $.clamp = (min: number, max: number) => - fluent(M.min(M.max(value, min), max)); - $.snapped = (multiple: number) => - fluent(M.round(value / multiple) * multiple); - - $.lerp = (b: number, t: number) => - fluent(value * (1 - t) + b * t); - - $.sin = () => fluent(M.sin(value)); - $.asin = () => fluent(M.asin(value)); - $.cos = () => fluent(M.cos(value)); - $.acos = () => fluent(M.acos(value)); - $.tan = () => fluent(M.tan(value)); - $.atan = () => fluent(M.atan(value)); - $.atan2 = (y: number) => fluent(M.atan2(y, value)); - - $.sinh = () => fluent(M.sinh(value)); - $.asinh = () => fluent(M.asinh(value)); - $.cosh = () => fluent(M.cosh(value)); - $.acosh = () => fluent(M.acosh(value)); - $.tanh = () => fluent(M.tanh(value)); - $.atanh = () => fluent(M.atanh(value)); - - const DEG_TO_RAD = M.PI / 180; - const RAD_TO_DEG = 180 / M.PI; - - $.to ??= {}; - assertType($.to); - Object.assign($.to, { - radians: () => fluent(value * DEG_TO_RAD), - degrees: () => fluent(value * RAD_TO_DEG), - }); - } else if ( - Array.isArray(value) && - value.every((x) => typeof x === "number") - ) { - applySwizzle(value, $, fluent); - } -}); - -if (import.meta.vitest) { - const { test, expect, expectTypeOf } = import.meta.vitest; - - const registry = [Math] as const; - const $ = makeFluent(registry); - - const t = (f: string, a: number, ...args: unknown[]) => { - test(f + "()", () => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call - expect(($(a) as any)[f](...args.slice(0, -1)).value).toBe( - args[args.length - 1], - ); - }); - }; - - t("add", 4, 5, 4 + 5); - t("subtract", 7, 2, 7 - 2); - t("multiply", 3, 3, 3 * 3); - t("pow", 2, 4, 2 ** 4); - t("sqrt", 121, M.sqrt(121)); - t("mod", 8, 3, 8 % 3); - test("log()", () => { - expect($(M.E).log().value).toBe(1); - expect($(1000).log(10).value).toBeCloseTo(M.log10(1000)); - }); - - test("comparesTo()", () => { - const a = 0.1 + 0.2; - const target = 0.3; - - expect($(a).comparesTo(target).value).toBe(true); - - expect($(a).comparesTo(1, 1).value).toBe(true); - - expect( - $(a).comparesTo(target + Number.EPSILON * 2).value, - ).toBe(false); - }); - - test("floor()", () => { - expect($(1).floor().value).toBe(1); - expect($(1.7).floor().value).toBe(1); - - expectTypeOf($(5.8).floor().value).toEqualTypeOf<5>(); - expectTypeOf($(5).floor().value).toEqualTypeOf<5>(); - }); - test("ceil()", () => { - expect($(1).ceil().value).toBe(1); - expect($(1.7).ceil().value).toBe(2); - - expectTypeOf($(5).ceil().value).toEqualTypeOf<5>(); - expectTypeOf($(5.8).ceil().value).toEqualTypeOf<6>(); - }); - test("round()", () => { - expect($(1.3).round().value).toBe(1); - expect($(4.5).round().value).toBe(5); - - expectTypeOf($(1.3).round().value).toEqualTypeOf<1>(); - expectTypeOf($(4.5).round().value).toEqualTypeOf<5>(); - }); - t("fround", 6.45, M.fround(6.45)); - - test("abs()", () => { - const positive = 4 as const; - const negative = -4 as const; - - const a = $(positive).abs().value; - expect(a).toBe(positive); - expectTypeOf(a).toEqualTypeOf(positive); - - const b = $(negative).abs().value; - expect(b).toBe(positive); - expectTypeOf(b).toEqualTypeOf(positive); - }); - test("negate()", () => { - const v = 7 as const; - - expect($(v).negate().value).toBe(-v); - expect($(v).negate().negate().value).toBe(v); - - expectTypeOf($(4).negate().value).toEqualTypeOf<-4>(); - expectTypeOf($(-4).negate().value).toEqualTypeOf<4>(); - }); - test("sign()", () => { - expect($(-4).sign().value).toBe(-1); - expect($(0).sign().value).toBe(0); - expect($(6).sign().value).toBe(1); - }); - - t("min", 3, 1, 2, 1); - t("max", 3, 1, 2, 3); - test("clamp()", () => { - const min = 1; - const max = 4; - - const v = 3; - - expect($(min - 4).clamp(min, max).value).toBe(min); - expect($(v).clamp(min, max).value).toBe(v); - expect($(max + 7).clamp(min, max).value).toBe(max); - }); - t("snapped", 12.345, 0.1, 12.3); - - test("lerp()", () => { - const a = 2; - const b = 4; - - expect($(a).lerp(b, 0).value).toBe(a); - expect($(a).lerp(b, 0.5).value).toBe(3); - expect($(a).lerp(b, 1).value).toBe(b); - - expect($(a).lerp(b, 2).value).toBe(b + (b - a)); - expect($(a).lerp(b, -1).value).toBe(0); - }); - - t("sin", M.PI, M.sin(M.PI)); - t("cos", M.PI, M.cos(M.PI)); - t("acos", M.PI, M.acos(M.PI)); - t("tan", M.PI, M.tan(M.PI)); - t("atan", M.PI, M.atan(M.PI)); - t("atan2", 10, M.PI, M.atan2(M.PI, 10)); - - t("sinh", M.PI, M.sinh(M.PI)); - t("asinh", M.PI, M.asinh(M.PI)); - t("cosh", M.PI, M.cosh(M.PI)); - t("acosh", M.PI, M.acosh(M.PI)); - t("tanh", M.PI, M.tanh(M.PI)); - t("atanh", 0.5, M.atanh(0.5)); - - test(".to.radians()", () => { - const angle = 180; - - expect($(angle).to.radians().value).toBe(M.PI); - }); - - test(".to.degrees()", () => { - const angle = M.PI; - - expect($(angle).to.degrees().value).toBe(180); - }); -} +export const Math = Mixin( + (value, $, fluent) => { + if (typeof value === "number") applyNumber(value, $, fluent); + else if ( + isArray(value) && + value.every((x) => typeof x === "number") + ) { + applySwizzle(value, $, fluent); + applyList(value, $, fluent); + } + }, +); diff --git a/src/mixin/math/base.ts b/src/mixin/math/base.ts new file mode 100644 index 0000000..be04486 --- /dev/null +++ b/src/mixin/math/base.ts @@ -0,0 +1,195 @@ +import type { Inc, Vec } from "../../utility"; + +export type Floor = + `${T}` extends `${infer N extends number}.${number}` ? N : T; +export type Ceil = + `${T}` extends `${infer N extends number}.${number}` ? Inc : T; +export type Round = + `${T}` extends `${infer N extends number}.${0 | 1 | 2 | 3 | 4}${number | ""}` + ? N + : Ceil; + +export type Sign = T extends 0 + ? 0 + : `${T}` extends `-${number}` + ? -1 + : 1; + +export type Abs = + `${T}` extends `-${infer N extends number}` ? N : T; + +export type Negate = T extends 0 + ? 0 + : `${T}` extends `-${infer N extends number}` + ? N + : `-${T}` extends `${infer N extends number}` + ? N + : number; + +type Fn< + N extends number = typeof Infinity, + Max extends boolean = false, +> = ( + ...args: [...Vec, ...(Max extends true ? [] : never[])] +) => unknown; + +interface IBase { + /** + * `x + y` + * @param other The second term of the addition (`y`) + */ + add: Fn<1>; + /** + * `x - y` + * @param other The second term of the subtraction (`y`) + */ + subtract: Fn<1>; + /** + * `x * y` + * @param factor Factor term of the multiplication (`y`) + */ + multiply: Fn<1>; + /** + * `x ** y` + * @param exponent Exponent term of the power (`y`) + */ + pow: Fn<1, true>; + /** + * `Math.sqrt(x)` (or `Math.log2(x)`) + */ + sqrt: Fn<0, true>; + /** + * `x / y` + * @param divisor Divisor term of the division (`y`) + */ + divide: Fn<1, true>; + /** + * `x mod y`, not to be confused with `x % y` (remainder operation) + * @param divisor Divisor term of the modulo operation (`y`) + * @see `.rem()` for the remainder operation + */ + mod: Fn<1, true>; + /** + * `x % y` remainder operation, not to be confused with `x mod y` (modulo) + * @param divisor Divisor term of the remainder operation (`y`) + * @see `.mod()` for the true modulo operation + */ + rem: Fn<1, true>; + /** + * `ln(x)`, `log(x, base)`, or alternatively `Math.log(x) / Math.log(base)` + * @param base Base of the logarithm (`base`). Defaults to `Math.E` for a natural logarithm function + */ + log: Fn<0, true>; + + /** + * `Math.floor(x)` + */ + floor: Fn<0, true>; + /** + * `Math.ceil(x)` + */ + ceil: Fn<0, true>; + /** + * `Math.round(x)` or `Math.round(x / multiple) * multiple` to round to the + * specified multiple + * @param multiple Multiple to round to, 1 if unspecified + */ + round: Fn<1, true>; + /** + * `Math.fround(x)`, round to the nearest 32-bit float + * approximation of value + */ + fround: Fn<0, true>; + /** + * `Math.f16round(x)`, round to the nearest 16-bit float + * approximation of value + */ + ffround: Fn<0, true>; + + /** + * `Math.abs(x)`, calculate the absolute value (distance + * from 0). + */ + abs: Fn<0, true>; + /** + * `Math.sign(x)`, returns the sign (1 or -1) of the value or zero + */ + sign: Fn<0, true>; + /** + * `-x` or alternatively `x * -1` + */ + negate: Fn<0, true>; + + /** + * `Math.min(x, ...values)` + * @param values Other canidates + */ + min: Fn<0>; + /** + * `Math.min(x, ...values)` + * @param values Other canidates + */ + max: Fn<0>; + + /** + * `Math.min(Math.max(x, min), max)` + * @param min Smallest allowed value to clamp to + * @param max Largest allowed value to clamp to + */ + clamp: Fn<2, true>; + /** + * Clamps value into the range [0.0, 1.0] + * @see `.clamp()` for a customizable range + */ + saturate: Fn<0, true>; + + /** + * Calculate the value at the specified point `t` on the line from the + * current value (`a`) corresponding to 0.0 to the target value (`b`) + * corresponding to 1.0. Linear interpolation from `a` to `b` at `t`. + * + * `t` is not constrained or clamped to the range [0.0, 1.0], values beyond + * that range extrapolate linearly beyond `a` and `b`. + * + * Equivalent to `lerp(a, b, t)`, `a * (1 - t) + b * t`, and `a + (b - a) * + * t` + * @param b Target value to interpolate to + * @param t Interpolation factor, the point on the line where 0.0 is `a` and 1.0 is `b` + * @see `.inverseLerp()` for the inverse operation (to calculate `t`) + */ + lerp: Fn<2, true>; + /** + * Normalize the value to the interpolation factor between the specified `a` + * and `b` range. This effectively computes the interpolation factor (`t`) + * of the linear interpolation `lerp(a, b, t)`. + * + * If the value is outside the bounds of the specified range, it will not be + * constrained or clamped to the range [0.0, 1.0]. + * + * Equivalent to a range normalization or `(v - a) / (b - a)` + * @param a The 0.0 point of the range + * @param b The 1.0 point of the range + * @see `.lerp()` for the inverse operation + */ + inverseLerp: Fn<2, true>; +} + +export type Base< + I extends IBase, + OmitDocKeys extends keyof IBase = never, +> = Omit<{ [K in keyof IBase]: unknown }, OmitDocKeys> & I; + +export namespace Base { + export interface Interface extends IBase {} +} + +export function log(x: number, base: number) { + if (base === 2) return Math.log2(x); + if (base === 10) return Math.log10(x); + if (base === Math.E) return Math.log(x); + return Math.log(x) / Math.log(base); +} + +export function clamp(x: number, min: number, max: number) { + return Math.min(Math.max(x, min), max); +} diff --git a/src/mixin/math/list.ts b/src/mixin/math/list.ts new file mode 100644 index 0000000..0c153c7 --- /dev/null +++ b/src/mixin/math/list.ts @@ -0,0 +1,277 @@ +import { makeFluent } from "../../base"; +import type { HKT } from "../../base/hkt"; +import { Mixin, type Props, type Return } from "../../base/mixin"; +import { assert, type HidePrototype } from "../../internal"; +import type { Apply, Vec } from "../../utility"; +import { Math as M } from "../math"; +import { + clamp, + log, + type Abs, + type Base, + type Ceil, + type Floor, + type Negate, + type Round, + type Sign, +} from "./base"; + +type Arg = Vec; + +interface FloorHkt extends HKT { + new: (t: HKT.T) => Floor; +} +interface CeilHkt extends HKT { + new: (t: HKT.T) => Ceil; +} +interface RoundHkt extends HKT { + new: (t: HKT.T) => Round; +} + +interface AbsHkt extends HKT { + new: (t: HKT.T) => Abs; +} +interface SignHkt extends HKT { + new: (t: HKT.T) => Sign; +} +interface NegateHkt extends HKT { + new: (t: HKT.T) => Negate; +} + +namespace IList { + export type BaseImpl< + T extends readonly number[], + t extends Props, + > = Base<{ + add: ( + other: number extends T["length"] + ? number + : number | Arg, + ) => Return, t>; + subtract: ( + other: number extends T["length"] + ? number + : number | Arg, + ) => Return, t>; + multiply: (factor: number) => Return, t>; + pow: (exponent: number) => Return, t>; + sqrt: () => Return, t>; + divide: (divisor: number) => Return, t>; + mod: (divisor: number) => Return, t>; + rem: (divisor: number) => Return, t>; + log: (base?: number) => Return, t>; + + floor: () => Return, t>; + ceil: () => Return, t>; + round: ( + ...multiple: N + ) => Return< + N extends readonly [number] ? Arg : Apply, + t + >; + fround: () => Return, t>; + ffround: () => Return, t>; + + abs: () => Return, t>; + sign: () => Return, t>; + negate: () => Return, t>; + + min: ( + ...others: U + ) => Return; + max: ( + ...others: U + ) => Return; + clamp: ( + min: Min, + max: Max, + ) => Return, T>, t>; + saturate: () => Return, t>; + + lerp: (b: Arg, t: number) => Return, t>; + inverseLerp: (a: Arg, b: Arg) => Return, t>; + }>; +} + +export type IList< + T extends readonly number[] = readonly number[], + t extends Props = Props, +> = T extends readonly [] | readonly [number] + ? {} + : IList.BaseImpl & { + sum: () => Return; + product: () => Return; + + findMin: () => Return; + findMax: () => Return; + + average: () => Return; + median: () => Return; + mode: () => Return; + range: () => Return; + + variance: (() => Return) & { + sample: () => Return; + } & HidePrototype; + deviation: (() => Return) & { + sample: () => Return; + } & HidePrototype; + }; + +export const applyList = Mixin.partial( + (value, $, fluent) => { + $.add = (other: number | number[]) => { + if (typeof other === "object") { + assert( + value.length === other.length, + "List.add() parameters must be of equal length", + ); + return fluent(value.map((x, idx) => x + other[idx])); + } + return fluent(value.map((x) => x + other)); + }; + $.subtract = (other: number | number[]) => { + if (typeof other === "object") { + assert( + value.length === other.length, + "List.subtract() parameters must be of equal length", + ); + return fluent(value.map((x, idx) => x - other[idx])); + } + return fluent(value.map((x) => x - other)); + }; + $.multiply = (factor: number) => + fluent(value.map((x) => x * factor)); + $.pow = (exponent: number) => + fluent(value.map((x) => x ** exponent)); + $.sqrt = () => fluent(value.map(Math.sqrt)); + $.divide = (divisor: number) => + fluent(value.map((x) => x / divisor)); + $.mod = (divisor: number) => + fluent( + value.map((x) => ((x % divisor) + divisor) % divisor), + ); + $.rem = (divisor: number) => + fluent(value.map((x) => x % divisor)); + $.log = (base: number = Math.E) => + fluent(value.map((x) => log(x, base))); + + $.floor = () => fluent(value.map(Math.floor)); + $.ceil = () => fluent(value.map(Math.ceil)); + $.round = (multiple?: number) => + fluent( + multiple !== undefined + ? value.map( + (x) => + Math.round(x / multiple) * multiple, + ) + : value.map(Math.floor), + ); + $.fround = () => fluent(value.map(Math.fround)); + $.ffround = () => fluent(value.map(Math.f16round)); + + $.abs = () => fluent(value.map(Math.abs)); + $.sign = () => fluent(value.map(Math.sign)); + $.negate = () => fluent(value.map((x) => -x)); + + $.min = (...others: number[]) => + fluent(Math.min(...value, ...others)); + $.max = (...others: number[]) => + fluent(Math.max(...value, ...others)); + $.clamp = (min: number, max: number) => + fluent(value.map((x) => clamp(x, min, max))); + $.saturate = () => fluent(value.map((x) => clamp(x, 0, 1))); + + $.lerp = (b: number[], t: number) => { + assert( + value.length === b.length, + "List.lerp() a and b parameters must be of equal length", + ); + return fluent( + value.map((x, idx) => x + (b[idx] - x) * t), + ); + }; + $.inverseLerp = (a: number[], b: number[]) => { + assert( + value.length === a.length && a.length === b.length, + "List.inverseLerp() a and b parameters must be of equal length", + ); + return fluent( + value.map( + (x, idx) => (x - a[idx]) / (b[idx] - a[idx]), + ), + ); + }; + + $.sum = () => fluent(value.reduce((a, b) => a + b, 0)); + $.product = () => fluent(value.reduce((a, b) => a * b, 1)); + + $.findMin = () => fluent(value.indexOf(Math.min(...value))); + $.findMax = () => fluent(value.indexOf(Math.max(...value))); + + $.average = () => + fluent(value.reduce((a, b) => a + b, 0) / value.length); + $.median = () => { + const sorted = value.toSorted((a, b) => a - b); + const middle = (sorted.length - 1) / 2; + + if (sorted.length % 2 === 1) return sorted[middle]; + + const a = sorted[Math.floor(middle)]; + const b = sorted[Math.ceil(middle)]; + + return (a + b) / 2; + }; + $.mode = () => { + const occurances = new Map(); + let max = 0; + + for (const v of value) { + const count = (occurances.get(v) ?? 0) + 1; + max = Math.max(max, count); + occurances.set(v, count); + } + + const output: number[] = []; + occurances.forEach((count, value) => { + if (count === max) output.push(value); + }); + + return fluent(output); + }; + + $.range = () => + fluent(Math.max(...value) - Math.min(...value)); + + const variance = (sample: boolean) => { + if (value.length === 0) return NaN; + if (value.length === 1) return sample ? NaN : 0; + + const mean = + value.reduce((a, b) => a + b, 0) / value.length; + const deviations = value.map((x) => (x - mean) ** 2); + + const sum = deviations.reduce((a, b) => a + b, 0); + + if (sample) return sum / (deviations.length - 1); + return sum / deviations.length; + }; + $.variance = Object.assign(() => fluent(variance(false)), { + sample: () => fluent(variance(true)), + }); + $.deviation = Object.assign( + () => fluent(Math.sqrt(variance(false))), + { + sample: () => fluent(Math.sqrt(variance(true))), + }, + ); + }, +); + +if (import.meta.vitest) { + const { test, expect, expectTypeOf, describe } = import.meta + .vitest; + + const registry = [M] as const; + const $ = makeFluent(registry); +} diff --git a/src/mixin/math/number.ts b/src/mixin/math/number.ts new file mode 100644 index 0000000..db273a3 --- /dev/null +++ b/src/mixin/math/number.ts @@ -0,0 +1,686 @@ +import { makeFluent } from "../../base"; +import { Mixin, type Props, type Return } from "../../base/mixin"; +import { Math as M } from "../math"; +import { + clamp, + log, + type Abs, + type Base, + type Ceil, + type Floor, + type Negate, + type Round, + type Sign, +} from "./base"; + +namespace INumber { + export type BaseImpl = Base<{ + /** + * @from {@link Math `Math`} + * @example + * ```ts + * const v = $(12).add(8).value; + * expect(v).toBe(20); + * ``` + */ + add: ( + /** Term to add */ + other: number, + ) => Return; + /** + * @from {@link Math `Math`} + * @example + * ```ts + * const v = $(12).subtract(8).value; + * expect(v).toBe(4); + * ``` + */ + subtract: ( + /** Term to subtract by */ + other: number, + ) => Return; + /** + * @from {@link Math `Math`} + * @example + * ```ts + * const v = $(12).multiply(8).value; + * expect(v).toBe(96) + * ``` + */ + multiply: ( + /** Factor to multiply by */ + factor: number, + ) => Return; + /** + * @from {@link Math `Math`} + * @example + * ```ts + * const v = $(12).pow(2).value; + * expect(v).toBe(144); + * ``` + */ + pow: ( + /** Exponent to raise to */ + exponent: number, + ) => Return; + /** + * @from {@link Math `Math`} + * @example + * ```ts + * const v = $(144).sqrt().value; + * expect(v).toBe(12); + * ``` + */ + sqrt: () => Return; + /** + * @from {@link Math `Math`} + * @example + * ```ts + * const v = $(12).divide(8).value; + * expect(v).toBe(1.5); + * ``` + */ + divide: ( + /** Divisor to divide by */ + divisor: number, + ) => Return; + /** + * @from {@link Math `Math`} + * @example + * ```ts + * const v = $(12).mod(8).value; + * expect(v).toBe(4); + * ``` + */ + mod: ( + /** Divisor to divide by */ + divisor: number, + ) => Return; + /** + * @from {@link Math `Math`} + * @example + * ```ts + * const v = $(12).rem(8).value; + * expect(v).toBe(4); + * ``` + */ + rem: ( + /** Divisor to divide by */ + divisor: number, + ) => Return; + /** + * @from {@link Math `Math`} + * @example + * ```ts + * const a = $(Math.E).log().value; + * expect(a).toBe(1); + * + * const b = $(125).log(5).value; + * expect(b).toBeCloseTo(3); + * ``` + */ + log: ( + /** The base of the logarithm */ + base?: number, + ) => Return; + + /** + * @from {@link Math `Math`} + * @example + * ```ts + * const v = $(1.7).floor().value; + * expect(v).toBe(1); + * ``` + */ + floor: () => Return, t>; + /** + * @from {@link Math `Math`} + * @example + * ```ts + * const v = $(1.3).ceil().value; + * expect(v).toBe(2); + * ``` + */ + ceil: () => Return, t>; + /** + * @from {@link Math `Math`} + * @example + * ```ts + * const a = $(1.3).round().value; + * const b = $(1.7).round().value; + * + * expect(a).toBe(1); + * expect(b).toBe(2); + * + * const c = $(1.7).round(1.5).value; + * const d = $(3.0).round(1.5).value; + * + * expect(c).toBe(1.5); + * expect(d).toBe(3.0); + * ``` + */ + round: ( + /** Unit multiple to round to */ + ...multiple: N + ) => Return< + N extends readonly [number] ? number : Round, + t + >; + /** + * @from {@link Math `Math`} + * @example + * ```ts + * const v = $(0.99999999).fround().value; + * expect(v).toBe(1); + * ``` + */ + fround: () => Return; + /** + * @from {@link Math `Math`} + * @example + * ```ts + * const v = $(0.9999).ffround().value; + * expect(v).toBe(1); + * ``` + */ + ffround: () => Return; + + /** + * @from {@link Math `Math`} + * @example + * ```ts + * const v = 4 as const; + * + * const a = $(v).abs().value; + * const b = $(-v).abs().value; + * + * expect(a).toBe(v); + * expect(b).toBe(v); + * ``` + */ + abs: () => Return, t>; + /** + * @from {@link Math `Math`} + * @example + * ```ts + * const v = 10 as const; + * + * const a = $(v).negate().value; + * expect(a).toBe(-v); + * + * const b = $(-v).negate().value; + * expect(b).toBe(v); + * ``` + */ + negate: () => Return, t>; + /** + * @from {@link Math `Math`} + * @example + * ```ts + * const a = $(-42).sign().value; + * const b = $(0).sign().value; + * const c = $(99).sign().value; + * + * expect(a).toBe(-1); + * expect(b).toBe(0); + * expect(c).toBe(1); + * ``` + */ + sign: () => Return, t>; + + /** + * @from {@link Math `Math`} + * @example + * ```ts + * const maximum = 10; + * + * const a = $(5).min(maximum).value; + * const b = $(15).min(maximum).value; + * + * expect(a).toBe(5); + * expect(b).toBe(10); + * ``` + */ + min: ( + /** Other candidates */ + ...values: U + ) => Return; + /** + * @from {@link Math `Math`} + * @example + * ```ts + * const minimum = 0; + * + * const a = $(5).max(minimum).value; + * const b = $(-5).max(minimum).value; + * + * expect(a).toBe(5); + * expect(b).toBe(0); + * ``` + */ + max: ( + /** Other candidates */ + ...values: U + ) => Return; + /** + * @from {@link Math `Math`} + * @example + * ```ts + * const min = 0; + * const max = 10; + * + * const a = $(5).clamp(min, max).value; + * const b = $(-2).clamp(min, max).value; + * const c = $(15).clamp(min, max).value; + * + * expect(a).toBe(5); + * expect(b).toBe(0); + * expect(c).toBe(10); + * ``` + */ + clamp: ( + /** The minimum (smallest) value of the range */ + min: Min, + /** The maximum (largest) value of the range */ + max: Max, + ) => Return; + /** + * @from {@link Math `Math`} + * @example + * ```ts + * const a = $(-2).saturate().value; + * const b = $(0.5).saturate().value; + * const c = $(3).saturate().value; + * + * expect(a).toBe(0.0); + * expect(b).toBe(0.5); + * expect(c).toBe(1.0); + * ``` + */ + saturate: () => Return; + + /** + * @from {@link Math `Math`} + * @example + * ```ts + * const from = 12; + * const to = 8; + * + * const a = $(from).lerp(to, 0).value; + * const b = $(from).lerp(to, 0.5).value; + * const c = $(from).lerp(to, 1).value; + * + * expect(a).toBe(12); + * expect(b).toBe(10); + * expect(c).toBe(8); + * + * const d = $(from).lerp(to, -1).value; + * const e = $(from).lerp(to, 2).value; + * + * expect(d).toBe(16); + * expect(e).toBe(4); + * ``` + */ + lerp: ( + /** Target value */ + b: number, + /** Interpolation factor, 0.0 corresponds to `a` and 1.0 corresponds to `b` */ + t: number, + ) => Return; + inverseLerp: ( + /** 0.0 point of the range */ + a: number, + /** 1.0 point of the range */ + b: number, + ) => Return; + }>; +} + +export interface INumber< + T extends number = number, + t extends Props = Props, +> extends INumber.BaseImpl { + /** + * Loosely compare value to the specified other value, + * accounting for floating-point inaccuracies. Comparision + * is performed within some degree of error. + * @param other Value to compare against + * @param delta The degree of acceptable error (defaults to `Number.EPSILON`) + * @from {@link Math `Math`} + * @example + * ```ts + * const result = 0.1 + 0.2; + * const target = 0.3; + * + * const a = $(result).comparesTo(target).value; + * expect(a).toBe(true); + * + * const b = $(result).comparesTo(1, 1).value; + * expect(b).toBe(true); + * + * const c = $(result).comparesTo(0).value; + * expect(c).toBe(false); + * ``` + */ + comparesTo: ( + /** Value to compare against */ + other: number, + /** Degree of acceptable error */ + delta?: number, + ) => Return; + + /** + * `Math.sin(x)` + * @from {@link Math `Math`} + */ + sin: () => Return; + /** + * `Math.asin(x)` + * @from {@link Math `Math`} + */ + asin: () => Return; + /** + * `Math.cos(x)` + * @from {@link Math `Math`} + */ + cos: () => Return; + /** + * `Math.acos(x)` + * @from {@link Math `Math`} + */ + acos: () => Return; + /** + * `Math.tan(x)` + * @from {@link Math `Math`} + */ + tan: () => Return; + /** + * `Math.atan(x)` + * @from {@link Math `Math`} + */ + atan: () => Return; + /** + * `Math.atan2(x, y)` + * @from {@link Math `Math`} + */ + atan2: (y: number) => Return; + + /** + * `Math.sinh(x)` + * @from {@link Math `Math`} + */ + sinh: () => Return; + /** + * `Math.asinh(x)` + * @from {@link Math `Math`} + */ + asinh: () => Return; + /** + * `Math.cosh(x)` + * @from {@link Math `Math`} + */ + cosh: () => Return; + /** + * `Math.acosh(x)` + * @from {@link Math `Math`} + */ + acosh: () => Return; + /** + * `Math.tanh(x)` + * @from {@link Math `Math`} + */ + tanh: () => Return; + /** + * `Math.atanh(x)` + * @from {@link Math `Math`} + */ + atanh: () => Return; + + to: { + /** + * Convert angle to representation in radians (`1 * + * Math.PI` radians per `1 * 180` degrees) + * @from {@link Math `Math`} + * @example + * ```ts + * const v = $(360).to.radians().value; + * expect(v).toBe(2 * Math.PI); + * ``` + */ + radians: () => Return; + /** + * Convert angle to representation in degrees (`1 * 180` + * degrees per `1 * Math.PI` radians) + * @from {@link Math `Math`} + * @example + * ```ts + * const v = $(2 * Math.PI).to.degrees().value; + * expect(v).toBe(360); + * ``` + */ + degrees: () => Return; + }; +} + +export const applyNumber = Mixin.partial( + (value, $, fluent) => { + $.add = (other: number) => fluent(value + other); + $.subtract = (other: number) => fluent(value - other); + $.multiply = (factor: number) => fluent(value * factor); + $.pow = (exponent: number) => + fluent(Math.pow(value, exponent)); + $.sqrt = () => fluent(Math.sqrt(value)); + $.divide = (divisor: number) => fluent(value / divisor); + $.rem = (divisor: number) => fluent(value % divisor); + $.mod = (divisor: number) => + fluent(((value % divisor) + divisor) % divisor); + $.log = (base = Math.E) => fluent(log(value, base)); + + $.comparesTo = ( + other: number, + delta: number = Number.EPSILON, + ) => { + return fluent(Math.abs(value - other) < delta); + }; + + $.floor = () => fluent(Math.floor(value)); + $.ceil = () => fluent(Math.ceil(value)); + $.round = (multiple?: number) => + fluent( + multiple === undefined + ? Math.round(value) + : Math.round(value / multiple) * multiple, + ); + $.fround = () => fluent(Math.fround(value)); + $.ffround = () => fluent(Math.f16round(value)); + + $.abs = () => fluent(Math.abs(value)); + $.negate = () => fluent(-value); + $.sign = () => fluent(Math.sign(value)); + + $.min = (...others: number[]) => + fluent(Math.min(value, ...others)); + $.max = (...others: number[]) => + fluent(Math.max(value, ...others)); + $.clamp = (min: number, max: number) => + fluent(clamp(value, min, max)); + $.saturate = () => fluent(clamp(value, 0, 1)); + + $.lerp = (b: number, t: number) => + fluent(value + (b - value) * t); + $.inverseLerp = (a: number, b: number) => + fluent((value - a) / (b - a)); + + $.sin = () => fluent(Math.sin(value)); + $.asin = () => fluent(Math.asin(value)); + $.cos = () => fluent(Math.cos(value)); + $.acos = () => fluent(Math.acos(value)); + $.tan = () => fluent(Math.tan(value)); + $.atan = () => fluent(Math.atan(value)); + $.atan2 = (y: number) => fluent(Math.atan2(y, value)); + + $.sinh = () => fluent(Math.sinh(value)); + $.asinh = () => fluent(Math.asinh(value)); + $.cosh = () => fluent(Math.cosh(value)); + $.acosh = () => fluent(Math.acosh(value)); + $.tanh = () => fluent(Math.tanh(value)); + $.atanh = () => fluent(Math.atanh(value)); + + const DEG_TO_RAD = Math.PI / 180; + const RAD_TO_DEG = 180 / Math.PI; + + $.to ??= {}; + $.to.radians = () => fluent(value * DEG_TO_RAD); + $.to.degrees = () => fluent(value * RAD_TO_DEG); + }, +); + +if (import.meta.vitest) { + const { test, expect, expectTypeOf } = import.meta.vitest; + + const registry = [M] as const; + const $ = makeFluent(registry); + + const t = (f: keyof INumber, a: number, ...args: unknown[]) => { + test(f + "()", () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + expect(($(a) as any)[f](...args.slice(0, -1)).value).toBe( + args[args.length - 1], + ); + }); + }; + + t("add", 4, 5, 4 + 5); + t("subtract", 7, 2, 7 - 2); + t("multiply", 3, 3, 3 * 3); + t("pow", 2, 4, 2 ** 4); + t("sqrt", 121, Math.sqrt(121)); + t("rem", -5, 3, -5 % 3); + t("mod", -5, 3, ((-5 % 3) + 3) % 3); + test("log()", () => { + expect($(Math.E).log().value).toBe(1); + expect($(1000).log(10).value).toBeCloseTo(Math.log10(1000)); + }); + + test("comparesTo()", () => { + const a = 0.1 + 0.2; + const target = 0.3; + + expect($(a).comparesTo(target).value).toBe(true); + + expect($(a).comparesTo(1, 1).value).toBe(true); + + expect( + $(a).comparesTo(target + Number.EPSILON * 2).value, + ).toBe(false); + }); + + test("floor()", () => { + expect($(1).floor().value).toBe(1); + expect($(1.7).floor().value).toBe(1); + + expectTypeOf($(5.8).floor().value).toEqualTypeOf<5>(); + expectTypeOf($(5).floor().value).toEqualTypeOf<5>(); + }); + test("ceil()", () => { + expect($(1).ceil().value).toBe(1); + expect($(1.7).ceil().value).toBe(2); + + expectTypeOf($(5).ceil().value).toEqualTypeOf<5>(); + expectTypeOf($(5.8).ceil().value).toEqualTypeOf<6>(); + }); + test("round()", () => { + expect($(1.3).round().value).toBe(1); + expect($(4.5).round().value).toBe(5); + + expectTypeOf($(1.3).round().value).toEqualTypeOf<1>(); + expectTypeOf($(4.5).round().value).toEqualTypeOf<5>(); + }); + t("fround", 6.45, Math.fround(6.45)); + t("ffround", 6.45, Math.f16round(6.45)); + + test("abs()", () => { + const positive = 4 as const; + const negative = -4 as const; + + const a = $(positive).abs().value; + expect(a).toBe(positive); + expectTypeOf(a).toEqualTypeOf(positive); + + const b = $(negative).abs().value; + expect(b).toBe(positive); + expectTypeOf(b).toEqualTypeOf(positive); + }); + test("negate()", () => { + const v = 7 as const; + + expect($(v).negate().value).toBe(-v); + expect($(v).negate().negate().value).toBe(v); + + expectTypeOf($(4).negate().value).toEqualTypeOf<-4>(); + expectTypeOf($(-4).negate().value).toEqualTypeOf<4>(); + }); + test("sign()", () => { + expect($(-4).sign().value).toBe(-1); + expect($(0).sign().value).toBe(0); + expect($(6).sign().value).toBe(1); + }); + + t("min", 3, 1, 2, 1); + t("max", 3, 1, 2, 3); + test("clamp()", () => { + const min = 1; + const max = 4; + + const v = 3; + + expect($(min - 4).clamp(min, max).value).toBe(min); + expect($(v).clamp(min, max).value).toBe(v); + expect($(max + 7).clamp(min, max).value).toBe(max); + }); + test("saturate()", () => { + expect($(-1).saturate().value).toBe(0); + expect($(0).saturate().value).toBe(0); + expect($(0.5).saturate().value).toBe(0.5); + expect($(1).saturate().value).toBe(1); + expect($(1.5).saturate().value).toBe(1); + }); + + test("lerp()", () => { + const a = 2; + const b = 4; + + expect($(a).lerp(b, 0).value).toBe(a); + expect($(a).lerp(b, 0.5).value).toBe(3); + expect($(a).lerp(b, 1).value).toBe(b); + + expect($(a).lerp(b, 2).value).toBe(b + (b - a)); + expect($(a).lerp(b, -1).value).toBe(0); + }); + + t("sin", Math.PI, Math.sin(Math.PI)); + t("cos", Math.PI, Math.cos(Math.PI)); + t("acos", Math.PI, Math.acos(Math.PI)); + t("tan", Math.PI, Math.tan(Math.PI)); + t("atan", Math.PI, Math.atan(Math.PI)); + t("atan2", 10, Math.PI, Math.atan2(Math.PI, 10)); + + t("sinh", Math.PI, Math.sinh(Math.PI)); + t("asinh", Math.PI, Math.asinh(Math.PI)); + t("cosh", Math.PI, Math.cosh(Math.PI)); + t("acosh", Math.PI, Math.acosh(Math.PI)); + t("tanh", Math.PI, Math.tanh(Math.PI)); + t("atanh", 0.5, Math.atanh(0.5)); + + test(".to.radians()", () => { + const angle = 180; + + expect($(angle).to.radians().value).toBe(Math.PI); + }); + + test(".to.degrees()", () => { + const angle = Math.PI; + + expect($(angle).to.degrees().value).toBe(180); + }); +} diff --git a/src/mixin/math/swizzle.ts b/src/mixin/math/swizzle.ts index e3a3595..d274ba4 100644 --- a/src/mixin/math/swizzle.ts +++ b/src/mixin/math/swizzle.ts @@ -1,5 +1,5 @@ import { makeFluent } from "../../base"; -import type { Props, Return } from "../../base/mixin"; +import { Mixin, type Props, type Return } from "../../base/mixin"; import { Math } from "../math"; import type { Vec, Dec, Inc as __Inc } from "../../utility"; @@ -98,11 +98,13 @@ type Primative< : never : TOut; +interface Swizzle4D extends SwizzlePermutations<4> {} + type SwizzleCache = [ { x: [0] }, SwizzlePermutations<2>, SwizzlePermutations<3>, - SwizzlePermutations<4>, + Swizzle4D, ]; export type Swizzle< @@ -110,38 +112,59 @@ export type Swizzle< t extends Props, > = number extends T["length"] ? unknown - : ( - Dec extends infer K extends 0 | 1 | 2 | 3 - ? { k: K; c: SwizzleCache[K] } - : SwizzleCache extends [...unknown[], infer Last] - ? { k: Dec; c: Last } - : never - ) extends { - k: infer K extends number; - c: infer C extends Record< - PropertyKey, - (number | undefined)[] - >; - } - ? Omit< - { - readonly [K in keyof C]: Return< - Sequence, - t + : T extends readonly [] + ? unknown + : ( + Dec extends infer K extends + | 0 + | 1 + | 2 + | 3 + ? { k: K; c: SwizzleCache[K] } + : SwizzleCache extends [ + ...unknown[], + infer Last, + ] + ? { + k: Dec; + c: Last; + } + : never + ) extends { + k: infer K extends number; + c: infer C extends Record< + PropertyKey, + (number | undefined)[] >; - }, - Axis[number] - > & - Primative[K] - : never; + } + ? Omit< + { + readonly [K in keyof C]: Return< + Sequence, + t + >; + }, + Axis[number] + > & + Primative[K] + : never; + +export type ISwizzle< + T extends readonly unknown[] = readonly unknown[], + t extends Props = Props, +> = { + readonly [K in keyof Swizzle4D]: Return< + Sequence, + t + >; +} & Primative[3]; const axis = ["x", "y", "z", "w"] as const; -export function applySwizzle( - value: number[], - $: object, - fluent: (value: unknown) => never, -) { +export const applySwizzle = Mixin.partial< + ISwizzle, + readonly unknown[] +>((value, $, fluent) => { const length = globalThis.Math.min(value.length, axis.length); const state = new Array(length).fill(-1); @@ -197,10 +220,11 @@ export function applySwizzle( return fluent(value[3]); }, }); -} +}); if (import.meta.vitest) { - const { test, expect, expectTypeOf } = import.meta.vitest; + const { test, describe, expect, expectTypeOf } = import.meta + .vitest; const registry = [Math] as const; const $ = makeFluent(registry); @@ -263,43 +287,49 @@ if (import.meta.vitest) { expectTypeOf(v).toEqualTypeOf(w); }); - test("Next", () => { - type N = 3; + describe("types", () => { + test("Next", () => { + type N = 3; - expectTypeOf< - Next - >().toEqualTypeOf<[0, undefined, undefined]>(); + expectTypeOf< + Next + >().toEqualTypeOf<[0, undefined, undefined]>(); - expectTypeOf< - Next - >().toEqualTypeOf<[undefined, 0, undefined]>(); + expectTypeOf< + Next + >().toEqualTypeOf<[undefined, 0, undefined]>(); - expectTypeOf>().toEqualTypeOf(); - }); + expectTypeOf>().toEqualTypeOf(); + }); - test("Key", () => { - expectTypeOf>().toEqualTypeOf<"yz">(); - expectTypeOf>().toEqualTypeOf<"yyy">(); - }); + test("Key", () => { + expectTypeOf< + Key<[1, 2, undefined]> + >().toEqualTypeOf<"yz">(); + expectTypeOf>().toEqualTypeOf<"yyy">(); + }); - test("Sequence", () => { - type In = [1, 2, 3, 4, 5]; - type Out = [5, 4, 3, 2, 1]; - type Reverse = [4, 3, 2, 1, 0]; + test("Sequence", () => { + type In = [1, 2, 3, 4, 5]; + type Out = [5, 4, 3, 2, 1]; + type Reverse = [4, 3, 2, 1, 0]; - expectTypeOf>().toEqualTypeOf(); - }); + expectTypeOf< + Sequence + >().toEqualTypeOf(); + }); - test("Primative", () => { - interface Props { - value: [number, number]; - meta: { registry: [] }; - } + test("Primative", () => { + interface Props { + value: [number, number]; + meta: { registry: [] }; + } - type V = Primative<[number, number], Props>[1]; + type V = Primative<[number, number], Props>[1]; - expectTypeOf().toHaveProperty("x"); - expectTypeOf().toHaveProperty("y"); - expectTypeOf().not.toHaveProperty("z"); + expectTypeOf().toHaveProperty("x"); + expectTypeOf().toHaveProperty("y"); + expectTypeOf().not.toHaveProperty("z"); + }); }); } diff --git a/src/mixin/optional.ts b/src/mixin/optional.ts index ec16354..d26d2e1 100644 --- a/src/mixin/optional.ts +++ b/src/mixin/optional.ts @@ -6,7 +6,7 @@ import { type Props, type Return, } from "../base/mixin"; -import { assert, assertType, type HidePrototype } from "../internal"; +import { assert, type HidePrototype } from "../internal"; type NoneSentinel = null | undefined; type None = Extract; @@ -16,211 +16,180 @@ function isNone(v: T): v is None { return v === null || v === undefined; } -interface Where { +interface IOptional { /** - * Set a value to a fallback if it does not conform to some conditional - * @param callback Callback returning a boolean, `false` sets the value to the fallback - * @param fallback The fallback value, `null` by default - * @example - * const parse = () => { version: "1.2.4" } as unknown; + * Transform a value via callback if it is not `null` or + * `undefined` * - * const version = $(parse()) - * .where(v => typeof v === 'object' && - * v !== null && - * 'version' in v) - * .and(v => v.version) - * .or("1.0.0") - * .value; - * expect(version).toBe("1.2.4"); + * If the value is equal to `null` or `undefined`, it + * remains unchanged and the callback is not called. + * @param callback Function for a non-null value + * @example + * const none = () => null as number | null; + * const some = () => 10 as number | null; + * + * const callback = (v: number) => v + 5; + * + * const a = $(none()).and(callback).value; + * expect(a).toBe(null) + * + * const b = $(some()).and(callback).value; + * expect(b).toBe(15); * @from {@link Optional `Optional`} */ - where: ( - callback: (v: T) => boolean, - fallback?: U, - ) => Return; + and: (callback: (v: Some) => U) => Return | U, t>; + /** + * Set value to a fallback if it is `null` or `undefined`. + * @param fallback The fallback value to use. To defer + * computing this fallback value, you can use `.or.else()` + * @example + * const none = () => null as number | null; + * const some = () => 10 as number | null; + * + * const fallback = -1; + * + * const a = $(none()).or(fallback).value; + * expect(a).toBe(fallback); + * + * const b = $(some()).or(fallback).value; + * expect(b).toBe(10); + * @from {@link Optional `Optional`} + */ + or: ((fallback: U) => Return | U, t>) & { + /** + * Set value to the result of `callback` if it is `null` + * or `undefined`. Unlike the normal `.or()`, this method + * only computes the fallback if the value is `null` or + * `undefined` + * @param callback + * @from {@link Optional `Optional`} + * @example + * ```ts + * const none = () => null as number | null; + * const some = () => 8 as number | null; + * + * const fallback = vi.fn(() => 22); // mocked + * + * const a = $(none()).or.else(fallback).value; + * expect(a).toBe(22); + * + * const b = $(some()).or.else(fallback).value; + * expect(b).toBe(8); + * + * expect(fallback).toHaveBeenCalledOnce() + * ``` + */ + else: ( + callback: (v: None) => U, + ) => Return | U, t>; + } & HidePrototype; + /** + * Assert that value is not `null` or `undefined` + * @param msg Reasoning to attach to the `AssertionError` + * @see `.assert.none()` for the inverse assertion + * @from {@link Optional `Optional`} + * @example + * ```ts + * const array = [1, 2, 3] as const; + * const element = array.at(1); + * + * const v = $(element).assert("index within bounds").value; + * expect(v).toBe(2); + * + * expect(() => { + * $(array.at(6)).assert() + * }).toThrow() + * ``` + */ + assert: ((msg?: string) => Return, t>) & { + /** + * Assert that the value is either `null` or `undefined` + * @param msg Reasoning to attach to the `AssertionError` + * @from {@link Optional `Optional`} + * @example + * ```ts + * const array = [1, 2, 3] as const; + * const element = array.at(5); + * + * const v = $(element).assert.none("index out of bounds").value; + * expect(v).toBe(undefined); + * + * expect(() => { + * $(array.at(1)).assert.none() + * }).toThrow() + * ``` + */ + none: (msg?: string) => Return, t>; + } & HidePrototype; } -export interface Optional extends Mixin.HKT { - new: (t: HKT.T) => Input extends infer T +export interface Optional extends Mixin.HKT { + new: ( + t: HKT.T, + ) => Input extends infer T ? None extends never - ? Where - : { - /** - * Transform a value via callback if it is not `null` or - * `undefined` - * - * If the value is equal to `null` or `undefined`, it - * remains unchanged and the callback is not called. - * @param callback Function for a non-null value - * @example - * const none = () => null as number | null; - * const some = () => 10 as number | null; - * - * const callback = (v: number) => v + 5; - * - * const a = $(none()).and(callback).value; - * expect(a).toBe(null) - * - * const b = $(some()).and(callback).value; - * expect(b).toBe(15); - * @from {@link Optional `Optional`} - */ - and: ( - callback: (v: Some) => U, - ) => Return | U, typeof t>; - /** - * Set value to a fallback if it is `null` or `undefined`. - * @param fallback The fallback value to use. To defer - * computing this fallback value, you can use `.or.else()` - * @example - * const none = () => null as number | null; - * const some = () => 10 as number | null; - * - * const fallback = -1; - * - * const a = $(none()).or(fallback).value; - * expect(a).toBe(fallback); - * - * const b = $(some()).or(fallback).value; - * expect(b).toBe(10); - * @from {@link Optional `Optional`} - */ - or: (( - fallback: U, - ) => Return | U, typeof t>) & { - /** - * Set value to the result of `callback` if it is `null` - * or `undefined`. Unlike the normal `.or()`, this method - * only computes the fallback if the value is `null` or - * `undefined` - * @param callback - * @from {@link Optional `Optional`} - * @example - * ```ts - * const none = () => null as number | null; - * const some = () => 8 as number | null; - * - * const fallback = vi.fn(() => 22); // mocked - * - * const a = $(none()).or.else(fallback).value; - * expect(a).toBe(22); - * - * const b = $(some()).or.else(fallback).value; - * expect(b).toBe(8); - * - * expect(fallback).toHaveBeenCalledOnce() - * ``` - */ - else: ( - callback: (v: None) => U, - ) => Return | U, typeof t>; - } & HidePrototype; - /** - * Assert that value is not `null` or `undefined` - * @param msg Reasoning to attach to the `AssertionError` - * @see `.assert.none()` for the inverse assertion - * @from {@link Optional `Optional`} - * @example - * ```ts - * const array = [1, 2, 3] as const; - * const element = array.at(1); - * - * const v = $(element).assert("index within bounds").value; - * expect(v).toBe(2); - * - * expect(() => { - * $(array.at(6)).assert() - * }).toThrow() - * ``` - */ - assert: (( - msg?: string, - ) => Return, typeof t>) & { - /** - * Assert that the value is either `null` or `undefined` - * @param msg Reasoning to attach to the `AssertionError` - * @from {@link Optional `Optional`} - * @example - * ```ts - * const array = [1, 2, 3] as const; - * const element = array.at(5); - * - * const v = $(element).assert.none("index out of bounds").value; - * expect(v).toBe(undefined); - * - * expect(() => { - * $(array.at(1)).assert.none() - * }).toThrow() - * ``` - */ - none: ( - msg?: string, - ) => Return, typeof t>; - } & HidePrototype; - } & Where + ? {} + : IOptional : never; } -export const Optional = Mixin((value, $, fluent) => { - if (isNone(value)) { - $.and = () => { - return fluent(value); - }; +export const Optional = Mixin( + (value, $, fluent) => { + if (isNone(value)) { + $.and = () => { + return fluent(value); + }; - $.or = (fallback: unknown) => { - return fluent(fallback); - }; - assertType($.or); - Object.assign($.or, { - else: (callback: (v: unknown) => unknown) => { + $.or = Object.assign( + (fallback: unknown) => { + return fluent(fallback); + }, + { + else: (callback: (v: unknown) => unknown) => { + return fluent(callback(value)); + }, + }, + ); + + $.assert = Object.assign( + (msg?: string) => { + assert(false, msg); + }, + { + none: () => { + return fluent(value); + }, + }, + ); + } else { + $.and = (callback: (v: unknown) => unknown) => { return fluent(callback(value)); - }, - }); + }; - $.assert = (msg?: string) => { - assert(false, msg); - }; - assertType($.assert); - Object.assign($.assert, { - none: () => { - return fluent(value); - }, - }); - } else { - $.and = (callback: (v: unknown) => unknown) => { - return fluent(callback(value)); - }; + $.or = Object.assign( + () => { + return fluent(value); + }, + { + else: () => { + return fluent(value); + }, + }, + ); - $.or = () => { - return fluent(value); - }; - - assertType($.or); - Object.assign($.or, { - else: () => { - return fluent(value); - }, - }); - - $.assert = () => { - return fluent(value); - }; - assertType($.assert); - Object.assign($.assert, { - none: (msg?: string) => { - assert(false, msg); - }, - }); - } - - $.where = ( - callback: (v: unknown) => boolean, - fallback = null, - ) => { - if (callback(value)) return fluent(value); - return fluent(fallback); - }; -}); + $.assert = Object.assign( + () => { + return fluent(value); + }, + { + none: (msg?: string) => { + assert(false, msg); + }, + }, + ); + } + }, +); if (import.meta.vitest) { const { test, expect, expectTypeOf, vi } = import.meta.vitest; @@ -272,16 +241,4 @@ if (import.meta.vitest) { expect(callback).toHaveBeenCalledOnce(); }); - - test("where()", () => { - const even = 4 as const; - const odd = 7 as const; - - const isEven = (v: number) => v % 2 === 0; - - expect($(even).where(isEven).value).toBe(even); - expect($(odd).where(isEven).value).toBe(null); - - expect($(odd).where(isEven, -1).value).toBe(-1); - }); } diff --git a/src/utility.ts b/src/utility.ts index b514cbb..cb6603b 100644 --- a/src/utility.ts +++ b/src/utility.ts @@ -1,8 +1,13 @@ +import type { HKT } from "./base/hkt"; + export type MaxDepth = 50; export type Pretty = { [K in keyof T]: T[K] }; export type Constrain = T extends U ? T : never; +export type IsReadonly = + T extends unknown[] ? true : false; + namespace Vec { // prettier-ignore type Lookup = N extends 0 @@ -181,6 +186,35 @@ namespace Flat { } export type Flat = Flat.Flat; +export type RecPartial = { + [K in keyof T]?: RecPartial; +}; + +export namespace Apply { + export interface Union extends HKT { + new: (t: HKT.T) => typeof t | U; + } + + export interface Intersection extends HKT { + new: (t: HKT.T) => typeof t & U; + } +} + +export type Apply< + Hkt extends HKT, + T extends readonly Hkt["_in"][], + TOut extends Hkt["_out"][] = [], +> = number extends T["length"] + ? Hkt["_out"][] + : TOut["length"] extends MaxDepth + ? (TOut[number] | Hkt["_out"])[] + : T extends readonly [ + infer Curr extends Hkt["_in"], + ...infer Rest extends Hkt["_in"][], + ] + ? Apply]> + : TOut; + if (import.meta.vitest) { const { test, expectTypeOf } = import.meta.vitest; diff --git a/tsconfig.app.json b/tsconfig.app.json index acad168..ab865b9 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -18,7 +18,6 @@ /* Linting */ "noUnusedLocals": false, "noUncheckedIndexedAccess": false, - "erasableSyntaxOnly": true, }, "include": ["src", "tests"] } diff --git a/tsconfig.node.json b/tsconfig.node.json index 4f0f2a0..4d2b796 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -12,6 +12,7 @@ "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "moduleDetection": "force", + "checkJs": true, "noEmit": true, /* Linting */