feat: docs

This commit is contained in:
2026-06-23 22:42:27 +02:00
parent edd6a68d9d
commit d1a1fbf85a
26 changed files with 5894 additions and 13 deletions
+144 -5
View File
@@ -80,9 +80,22 @@ namespace At {
type At<
T extends readonly unknown[],
K extends keyof T & number,
> = `${K}` extends `-${infer N extends number}`
? At.Select<T, At.Dec<N>>
: T[K];
> = number extends T["length"]
? T[K] | undefined
: `${K}` extends `-${infer N extends number}`
? At.Select<T, At.Dec<N>>
: T[K];
type Repeat<
T extends readonly unknown[],
N extends number,
Acc extends null[] = [],
TOut extends readonly T[number][] = [],
> = Acc["length"] extends N
? [...TOut, ...T]
: Acc["length"] extends MaxDepth
? T[number][]
: Repeat<T, N, [...Acc, null], [...TOut, ...T]>;
namespace Flat {
type Inc<
@@ -124,18 +137,112 @@ export interface Array extends Mixin.HKT {
new: (t: HKT.T<this>) => Input<typeof t> extends infer T
? T extends readonly (infer Item)[]
? {
/**
* Index the array using the specified zero-based index.
* Negative indexes start at the end of the array.
* @param index Zero-based index
* @example
* 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);
* @from {@link Array `Array`}
*/
at: <const K extends keyof T & number>(
index: K,
) => Return<At<T, K>, typeof t>;
/**
* Interospect each item of the array using the specified
* callback
* @param callback Callback taking item, index, and the array for each item.
* @example
* 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);
* @from {@link Array `Array`}
*/
each: (
callback: (...args: IterArgs<T>) => void,
) => Return<T, typeof t>;
/**
* Transform each item of the array using the specified callback
* @param callback Transformative callback taking item, index, and array; returns the new item
* @example
* 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"]);
* @from {@link Array `Array`}
*/
map: <U>(
callback: (...args: IterArgs<T>) => U,
) => Return<Map<T, U>, typeof t>;
filter: (
/**
* Extend the array by repeating its current contents n times
* @param count The number of repetitions to add
* @example
* const array = [1, 2];
*
* const v = $(array).extend(2).value;
* expect(v).toMatchObject([1, 2, 1, 2, 1, 2]);
* @from {@link Array `Array`}
*/
extend: <const N extends number>(
count: N,
) => Return<Repeat<T, N>, typeof t>;
/**
* Filter the array to contain only items satisfying the
* specified callback conditional
* @param callback Callback getting item, index, and array for each item and determining if it should be filtered out or not
* @example
* const dirty = [-2, 4, 1, -5, -6] as const;
*
* const v = $(dirty).filter(v => v >= 0).value;
* expect(v).toMatchObject([4, 1]);
* @from {@link Array `Array`}
*/
filter: ((
callback: (...args: IterArgs<T>) => boolean,
) => Return<Unordered<T>, typeof t>;
) => Return<Unordered<T>, typeof t>) & {
/**
* Filter the array to only contain items which are not
* `null` or `undefined`.
* @example
* 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]);
* @from {@link Array `Array`}
*/
some: () => Return<
T extends unknown[]
? Exclude<
T[number],
null | undefined
>[]
: readonly Exclude<
T[number],
null | undefined
>[],
typeof t
>;
} & HidePrototype;
reduce: (<U>(
initial: U,
callback: (
@@ -217,6 +324,14 @@ export const Array = Mixin<Array>((value, $, fluent) => {
return fluent(value);
};
$.extend = (count: number) => {
assert(count >= 0);
if (count === 0) return fluent(value);
return fluent(
value.concat(...new globalThis.Array(count).fill(value)),
);
};
$.map = (callback: (...args: IterArgs) => unknown) => {
return fluent(value.map(callback));
};
@@ -224,6 +339,14 @@ export const Array = Mixin<Array>((value, $, fluent) => {
$.filter = (callback: (...args: IterArgs) => boolean) => {
return fluent(value.filter(callback));
};
assertType<object>($.filter);
Object.assign($.filter, {
some: () => {
return fluent(
value.filter((v) => v !== null && v !== undefined),
);
},
});
$.reduce = (
initial: unknown,
@@ -326,6 +449,17 @@ if (import.meta.vitest) {
);
});
test("extend()", () => {
const arr = [1, 2] as const;
const copies = 2;
const expected: number[] = new globalThis.Array(copies + 1)
.fill(arr)
.flat(1);
expect($(arr).extend(copies).value).toMatchObject(expected);
});
test("map()", () => {
const arr = [1, 2, 3, 4, 5] as const;
const isEven = vi.fn((v: number) => v % 2 === 0);
@@ -358,6 +492,11 @@ if (import.meta.vitest) {
i,
arr,
);
const dirty = [1, null, 6, undefined, 2] as const;
expect($(dirty).filter.some().value).toMatchObject(
dirty.filter((v) => v !== null && v !== undefined),
);
});
test("reduce()", () => {
+26 -1
View File
@@ -5,9 +5,34 @@ import { Mixin, type Input, type Return } from "../base/mixin";
export interface Base extends Mixin.HKT {
new: (t: HKT.T<this>) => Input<typeof t> extends infer T
? {
/**
* Interospect value using the specified `callback` without
* modifying the value.
* @param callback The interospective callback
* @see `transform` to modify
* @example
* let x;
* const value = $(10).tap(v => { x = ++v }).value;
*
* expect(x).toBe(11);
* expect(value).toBe(10);
* @from {@link Base `Base`}
*/
tap(
callback: (value: T) => void,
callback: (value: Readonly<T>) => void,
): Return<T, typeof t>;
/**
* 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
* @example
* const value = $("Hello")
* .transform(v => v.toUpperCase())
* .value;
* expect(value).toBe("HELLO");
* @from {@link Base `Base`}
*/
transform<U>(
callback: (value: T) => U,
): Return<U, typeof t>;
View File
+137 -4
View File
@@ -6,7 +6,7 @@ import {
type Props,
type Return,
} from "../base/mixin";
import { assertType, type HidePrototype } from "../internal";
import { assert, assertType, type HidePrototype } from "../internal";
type NoneSentinel = null | undefined;
type None<T> = Extract<T, NoneSentinel>;
@@ -17,6 +17,23 @@ function isNone<T>(v: T): v is None<T> {
}
interface Where<T, t extends Props> {
/**
* 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;
*
* 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");
* @from {@link Optional `Optional`}
*/
where: <U = null>(
callback: (v: T) => boolean,
fallback?: U,
@@ -28,16 +45,111 @@ export interface Optional extends Mixin.HKT {
? None<T> extends never
? Where<T, typeof t>
: {
/**
* 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: <U>(
callback: (v: Some<T>) => U,
) => Return<None<T> | 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: (<U>(
fallback: U,
) => Return<Some<T> | 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
* @example
* const none = () => null as number | null;
* const some = () => 8 as number | null;
*
* const fallback = () => 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()
* @from {@link Optional `Optional`}
*/
else: <U>(
callback: (v: None<T>) => U,
) => Return<Some<T> | U, typeof t>;
} & HidePrototype;
/**
* Assert that value is not `null` or `undefined`
* @param msg Reasoning to attach to the `AssertionError`
* @example
* const array = [1, 2, 3];
* const element: number | undefined = array[1];
*
* const v = $(element).assert("index within bounds").value;
* expect(v).toBe(2);
*
* expect(() => {
* $(array[6]).assert()
* }).toThrow()
* @see `.assert.none()` for the inverse assertion
* @from {@link Optional `Optional`}
*/
assert: ((
msg?: string,
) => Return<Some<T>, typeof t>) & {
/**
* Assert that the value is either `null` or `undefined`
* @param msg Reasoning to attach to the `AssertionError`
* @example
* const array = [1, 2, 3];
* const element: number | undefined = array[5];
*
* const v = $(element).assert.none("index out of bounds").value;
* expect(v).toBe(undefined);
*
* expect(() => {
* $(array[1]).assert.none()
* }).toThrow()
*/
none: (
msg?: string,
) => Return<None<T>, typeof t>;
} & HidePrototype;
} & Where<T, typeof t>
: never;
}
@@ -51,13 +163,22 @@ export const Optional = Mixin<Optional>((value, $, fluent) => {
$.or = (fallback: unknown) => {
return fluent(fallback);
};
assertType<object>($.or);
Object.assign($.or, {
else: (callback: (v: unknown) => unknown) => {
return fluent(callback(value));
},
});
$.assert = () => {
return fluent(value);
};
assertType<object>($.assert);
Object.assign($.assert, {
none: (msg?: string) => {
assert(false, msg);
},
});
} else {
$.and = (callback: (v: unknown) => unknown) => {
return fluent(callback(value));
@@ -73,6 +194,16 @@ export const Optional = Mixin<Optional>((value, $, fluent) => {
return fluent(value);
},
});
$.assert = (msg?: string) => {
assert(false, msg);
};
assertType<object>($.assert);
Object.assign($.assert, {
none: (msg?: string) => {
return fluent(value);
},
});
}
$.where = (
@@ -85,7 +216,7 @@ export const Optional = Mixin<Optional>((value, $, fluent) => {
});
if (import.meta.vitest) {
const { test, expect } = import.meta.vitest;
const { test, expect, vi } = import.meta.vitest;
const registry = [Optional] as const;
const $ = makeFluent(registry);
@@ -112,10 +243,12 @@ if (import.meta.vitest) {
expect($(some).or(fallback).value).toBe(some);
expect($(none).or(fallback).value).toBe(fallback);
const callback = () => fallback;
const callback = vi.fn(() => fallback);
expect($(some).or.else(callback).value).toBe(some);
expect($(none).or.else(callback).value).toBe(fallback);
expect(callback).toHaveBeenCalledOnce();
});
test("where()", () => {