feat: typechecking tests

This commit is contained in:
2026-06-27 00:37:01 +02:00
parent a9925ac2ec
commit 408806d17b
28 changed files with 2091 additions and 1456 deletions
+29
View File
@@ -18,3 +18,32 @@ export namespace HKT {
(T & { _t: t })["new"]
>;
}
if (import.meta.vitest) {
const { test, expectTypeOf } = import.meta.vitest;
interface Identity extends HKT {
new: (t: HKT.T<this>) => typeof t;
}
interface ReplaceWith<T extends number> extends HKT<number, T> {
new: () => T;
}
test("Identity <T> = T", () => {
type V = 10;
expectTypeOf<HKT.Apply<Identity, V>>().toEqualTypeOf<V>();
expectTypeOf<
HKT.Apply<Identity, Identity>
>().toEqualTypeOf<Identity>();
});
test("Generic <T><U> = T", () => {
type V = 10;
expectTypeOf<
HKT.Apply<ReplaceWith<V>, 14>
>().toEqualTypeOf<V>();
});
}
+43 -27
View File
@@ -336,6 +336,10 @@ export interface Array extends Mixin.HKT {
* @param index The index of the current element in the array
* @param array The array being iterated
*/
callback: (
value: U,
...args: IterArgs<T>
) => U,
) => Return<U, typeof t>;
} & HidePrototype;
/**
@@ -565,7 +569,7 @@ export const Array = Mixin<Array>((value, $, fluent) => {
});
if (import.meta.vitest) {
const { test, expect, vi } = import.meta.vitest;
const { test, expect, expectTypeOf, vi } = import.meta.vitest;
const registry = [Array] as const;
const $ = makeFluent(registry);
@@ -573,10 +577,20 @@ if (import.meta.vitest) {
test("at()", () => {
const arr = [1, 2, 3] as const;
expect($(arr).at(0).value).toBe(arr[0]);
const first = $(arr).at(0).value;
expect(first).toBe(arr[0]);
expectTypeOf(first).toEqualTypeOf(arr[0]);
expect($(arr).at(5).value).toBe(undefined);
expect($(arr).at(-1).value).toBe(arr[arr.length - 1]);
const last = $(arr).at(-1).value;
expect(last).toBe(arr[arr.length - 1]);
expectTypeOf(last).toEqualTypeOf<
typeof arr extends readonly [...unknown[], infer Last]
? Last
: never
>();
expect($(arr).at(-5).value).toBe(undefined);
});
@@ -598,13 +612,13 @@ if (import.meta.vitest) {
});
test("extend()", () => {
const arr = [1, 2];
const arr = [1, 2] as const;
const copies = 2;
const copies = 2 as const;
const expected: number[] = new globalThis.Array<number[]>(
copies + 1,
)
.fill(arr)
.fill([...arr])
.flat(1);
expect($(arr).extend(copies).value).toMatchObject(expected);
@@ -615,9 +629,10 @@ if (import.meta.vitest) {
const arr = [1, 2, 3, 4, 5] as const;
const isEven = vi.fn((v: number) => v % 2 === 0);
expect($(arr).map(isEven).value).toMatchObject(
arr.map(isEven),
);
const v = $(arr).map(isEven).value;
expect(v).toMatchObject(arr.map(isEven));
expectTypeOf(v).toExtend<boolean[]>();
expectTypeOf(v).not.toEqualTypeOf<boolean[]>();
for (let i = 0; i < arr.length; i++)
expect(isEven).toHaveBeenNthCalledWith(
@@ -632,9 +647,10 @@ if (import.meta.vitest) {
const arr = [5, 4, 3, 2, 1] as const;
const isEven = vi.fn((v: number) => v % 2 === 0);
expect($(arr).filter(isEven).value).toMatchObject(
arr.filter(isEven),
);
const v = $(arr).filter(isEven).value;
expect(v).toMatchObject(arr.filter(isEven));
expectTypeOf(v).toExtend<readonly number[]>();
expectTypeOf(v).not.toEqualTypeOf<readonly number[]>();
for (let i = 0; i < arr.length; i++)
expect(isEven).toHaveBeenNthCalledWith(
@@ -645,9 +661,12 @@ if (import.meta.vitest) {
);
const dirty = [1, null, 6, undefined, 2] as const;
expect($(dirty).filter.some().value).toMatchObject(
const clean = $(dirty).filter.some().value;
expect(clean).toMatchObject(
dirty.filter((v) => v !== null && v !== undefined),
);
expectTypeOf(clean).toExtend<readonly number[]>();
expectTypeOf(clean).not.toEqualTypeOf<readonly number[]>();
});
test("reduce()", () => {
@@ -693,7 +712,9 @@ if (import.meta.vitest) {
test("length()", () => {
const arr = [3, 3, 3] as const;
expect($(arr).length().value).toBe(arr.length);
const v = $(arr).length().value;
expect(v).toBe(arr.length);
expectTypeOf(v).toEqualTypeOf(arr.length);
});
test("sort()", () => {
@@ -701,9 +722,10 @@ if (import.meta.vitest) {
const callback = (a: number) => (a % 2 === 0 ? -1 : 1);
expect($(numbers).sort(callback).value).toMatchObject(
numbers.toSorted(callback),
);
const v = $(numbers).sort(callback).value;
expect(v).toMatchObject(numbers.toSorted(callback));
expectTypeOf(v).toExtend<readonly number[]>();
expectTypeOf(v).not.toEqualTypeOf<readonly number[]>();
expect($(numbers).sort.ascending().value).toMatchObject(
numbers.toSorted((a, b) => a - b),
@@ -736,15 +758,9 @@ if (import.meta.vitest) {
test("reverse()", () => {
const array = [1, 2, 3, 4, 5] as const;
expect($(array).reverse().value).toMatchObject(
array.toReversed(),
);
});
test("length()", () => {
const length = 3;
const array = new globalThis.Array<number>(length).fill(5);
expect($(array).length().value).toBe(length);
const v = $(array).reverse().value;
expect(v).toMatchObject(array.toReversed());
expectTypeOf(v).toExtend<readonly number[]>();
expectTypeOf(v).not.toEqualTypeOf<readonly number[]>();
});
}
+1 -1
View File
@@ -109,7 +109,7 @@ if (import.meta.vitest) {
const $ = makeFluent(registry);
test(".awaited", async () => {
const value = 10;
const value = 10 as const;
const promise = new Promise<number>((r) => {
r(value);
});
+77 -19
View File
@@ -6,15 +6,44 @@ import {
type Props,
type Return,
} from "../base/mixin";
import { assertType } from "../internal";
import { assertType, type MaxDepth } from "../internal";
import { applySwizzle, type Swizzle } from "./math/swizzle";
type Vec<
T,
N extends number,
TOut extends T[] = [],
> = TOut["length"] extends N
? TOut
: TOut["length"] extends MaxDepth
? T[]
: Vec<T, N, [...TOut, T]>;
type Inc<N extends number> = [...Vec<null, N>, null]["length"];
type Sign<T extends number> = T extends 0
? 0
: `${T}` extends `-${number}`
? -1
: 1;
type Negate<T extends number> = T extends 0
? 0
: `${T}` extends `-${infer N extends number}`
? N
: `-${T}` extends `${infer N extends number}`
? N
: number;
type Floor<T extends number> =
`${T}` extends `${infer N extends number}.${number}` ? N : T;
type Ceil<T extends number> =
`${T}` extends `${infer N extends number}.${number}` ? Inc<N> : T;
type Round<T extends number> =
`${T}` extends `${infer N extends number}.${0 | 1 | 2 | 3 | 4}${number | ""}`
? N
: Ceil<T>;
export interface Math extends Mixin.HKT {
new: (t: HKT.T<this>) => Input<typeof t> extends infer T
? T extends number
@@ -38,18 +67,23 @@ export interface Math extends Mixin.HKT {
) => Return<number, typeof t>;
log: (base?: number) => Return<number, typeof t>;
isEqualApprox: (
comparesTo: (
other: number,
delta?: number,
) => Return<boolean, typeof t>;
floor: () => Return<number, typeof t>;
ceil: () => Return<number, typeof t>;
round: () => Return<number, typeof t>;
floor: () => Return<Floor<T>, typeof t>;
ceil: () => Return<Ceil<T>, typeof t>;
round: () => Return<Round<T>, typeof t>;
fround: () => Return<number, typeof t>;
abs: () => Return<number, typeof t>;
negate: () => Return<number, typeof t>;
abs: () => Return<
`${T}` extends `-${infer N extends number}`
? N
: T,
typeof t
>;
negate: () => Return<Negate<T>, typeof t>;
sign: () => Return<Sign<T>, typeof t>;
min: (
@@ -118,7 +152,7 @@ export const Math = Mixin<Math>((value, $, fluent) => {
return fluent(M.log(value));
};
$.isEqualApprox = (
$.comparesTo = (
other: number,
delta: number = Number.EPSILON,
) => {
@@ -179,7 +213,7 @@ export const Math = Mixin<Math>((value, $, fluent) => {
});
if (import.meta.vitest) {
const { test, expect } = import.meta.vitest;
const { test, expect, expectTypeOf } = import.meta.vitest;
const registry = [Math] as const;
const $ = makeFluent(registry);
@@ -204,38 +238,62 @@ if (import.meta.vitest) {
expect($(1000).log(10).value).toBeCloseTo(M.log10(1000));
});
test("equalApprox()", () => {
test("comparesTo()", () => {
const a = 0.1 + 0.2;
const target = 0.3;
expect($(a).isEqualApprox(target).value).toBe(true);
expect($(a).comparesTo(target).value).toBe(true);
expect($(a).isEqualApprox(1, 1).value).toBe(true);
expect($(a).comparesTo(1, 1).value).toBe(true);
expect(
$(a).isEqualApprox(target + Number.EPSILON * 2).value,
$(a).comparesTo(target + Number.EPSILON * 2).value,
).toBe(false);
});
t("floor", 1.7, 1);
t("ceil", 1.7, 2);
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 v = 4;
const positive = 4 as const;
const negative = -4 as const;
expect($(v).abs().value).toBe(v);
expect($(-v).abs().value).toBe(v);
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;
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);
+58 -7
View File
@@ -160,7 +160,7 @@ export function applySwizzle(
$: object,
fluent: (value: unknown) => never,
) {
const length = value.length;
const length = globalThis.Math.min(value.length, axis.length);
const state = new Array<number>(length).fill(-1);
main: do {
@@ -218,7 +218,7 @@ export function applySwizzle(
}
if (import.meta.vitest) {
const { test, expect } = import.meta.vitest;
const { test, expect, expectTypeOf } = import.meta.vitest;
const registry = [Math] as const;
const $ = makeFluent(registry);
@@ -230,9 +230,12 @@ if (import.meta.vitest) {
const vec = [x, y] as const;
expect($(vec).xx.value).toMatchObject([x, x]);
expect($(vec).yx.value).toMatchObject([y, x]);
expect($(vec).yy.value).toMatchObject([y, y]);
const yx = $(vec).yx.value;
expect(yx).toMatchObject([y, x]);
expectTypeOf(yx).toEqualTypeOf<[typeof y, typeof x]>();
const large = [x, y, x, y] as const;
expect($(large).wzyx.value).toMatchObject([y, x, y, x]);
@@ -246,27 +249,75 @@ if (import.meta.vitest) {
const x = 5 as const;
const arr = [x] as const;
expect($(arr).x.value).toBe(x);
const v = $(arr).x.value;
expect(v).toBe(x);
expectTypeOf(v).toEqualTypeOf(x);
});
test(".y", () => {
const y = 2 as const;
const arr = [y, y] as const;
expect($(arr).y.value).toBe(y);
const v = $(arr).y.value;
expect(v).toBe(y);
expectTypeOf(v).toEqualTypeOf(y);
});
test(".z", () => {
const z = 9 as const;
const arr = [z, z, z] as const;
expect($(arr).z.value).toBe(z);
const v = $(arr).z.value;
expect(v).toBe(z);
expectTypeOf(v).toEqualTypeOf(z);
});
test(".w", () => {
const w = 0 as const;
const arr = [w, w, w, w] as const;
expect($(arr).w.value).toBe(w);
const v = $(arr).w.value;
expect(v).toBe(w);
expectTypeOf(v).toEqualTypeOf(w);
});
test("Next", () => {
type N = 3;
expectTypeOf<
Next<N, [undefined, undefined, undefined]>
>().toEqualTypeOf<[0, undefined, undefined]>();
expectTypeOf<
Next<N, [2, undefined, undefined]>
>().toEqualTypeOf<[undefined, 0, undefined]>();
expectTypeOf<Next<N, [2, 2, 2]>>().toEqualTypeOf<null>();
});
test("Key", () => {
expectTypeOf<Key<[1, 2, undefined]>>().toEqualTypeOf<"yz">();
expectTypeOf<Key<[1, 1, 1]>>().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];
expectTypeOf<Sequence<In, Reverse>>().toEqualTypeOf<Out>();
});
test("Primative", () => {
interface Props {
value: [number, number];
meta: { registry: [] };
}
type V = Primative<[number, number], Props>[1];
expectTypeOf<V>().toHaveProperty("x");
expectTypeOf<V>().toHaveProperty("y");
expectTypeOf<V>().not.toHaveProperty("z");
});
}
+34 -19
View File
@@ -34,7 +34,7 @@ interface Where<T, t extends Props> {
* expect(version).toBe("1.2.4");
* @from {@link Optional `Optional`}
*/
where: <U = null>(
where: <const U = null>(
callback: (v: T) => boolean,
fallback?: U,
) => Return<T | U, t>;
@@ -122,14 +122,14 @@ export interface Optional extends Mixin.HKT {
* @from {@link Optional `Optional`}
* @example
* ```ts
* const array = [1, 2, 3];
* const element: number | undefined = array[1];
* 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[6]).assert()
* $(array.at(6)).assert()
* }).toThrow()
* ```
*/
@@ -142,14 +142,14 @@ export interface Optional extends Mixin.HKT {
* @from {@link Optional `Optional`}
* @example
* ```ts
* const array = [1, 2, 3];
* const element: number | undefined = array[5];
* 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[1]).assert.none()
* $(array.at(1)).assert.none()
* }).toThrow()
* ```
*/
@@ -223,17 +223,17 @@ export const Optional = Mixin<Optional>((value, $, fluent) => {
});
if (import.meta.vitest) {
const { test, expect, vi } = import.meta.vitest;
const { test, expect, expectTypeOf, vi } = import.meta.vitest;
const registry = [Optional] as const;
const $ = makeFluent(registry);
type Value = number | null;
const Value = <const T>(v: T) => v as T | null;
test("and()", () => {
const value = 10;
const some = value as Value;
const none = null as Value;
const some = Value(value);
const none = Value(null);
const callback = (v: number) => v + 5;
@@ -242,25 +242,40 @@ if (import.meta.vitest) {
});
test("or()", () => {
const some = 10 as Value;
const none = null as Value;
const value = 10 as const;
const some = Value(value);
const none = Value(null);
const fallback = 15 as const;
expect($(some).or(fallback).value).toBe(some);
expect($(none).or(fallback).value).toBe(fallback);
const a = $(some).or(fallback).value;
expect(a).toBe(some);
expectTypeOf(a).toEqualTypeOf<
typeof value | typeof fallback
>();
const b = $(none).or(fallback).value;
expect(b).toBe(fallback);
expectTypeOf(b).toEqualTypeOf<typeof fallback>();
const callback = vi.fn(() => fallback);
expect($(some).or.else(callback).value).toBe(some);
expect($(none).or.else(callback).value).toBe(fallback);
const c = $(some).or.else(callback).value;
expect(c).toBe(some);
expectTypeOf(c).toEqualTypeOf<
typeof value | typeof fallback
>();
const d = $(none).or.else(callback).value;
expect(d).toBe(fallback);
expectTypeOf(d).toEqualTypeOf<typeof fallback>();
expect(callback).toHaveBeenCalledOnce();
});
test("where()", () => {
const even = 4;
const odd = 7;
const even = 4 as const;
const odd = 7 as const;
const isEven = (v: number) => v % 2 === 0;