feat: more mixins
This commit is contained in:
+2
-2
@@ -1,6 +1,6 @@
|
||||
export interface HKT<In = unknown, Out = unknown> {
|
||||
readonly _meta: In;
|
||||
readonly _t: In;
|
||||
readonly _t: unknown;
|
||||
new: (t: never) => Out;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ type Param<
|
||||
export namespace HKT {
|
||||
export type T<
|
||||
This extends HKT,
|
||||
T = This extends { _t: infer I } ? I : unknown,
|
||||
T = This extends { _meta: infer I } ? I : unknown,
|
||||
> = Param<This, T>;
|
||||
export type Apply<T extends HKT, t extends T["_t"]> = ReturnType<
|
||||
(T & { _t: t })["new"]
|
||||
|
||||
+7
-4
@@ -1,18 +1,21 @@
|
||||
import type { DefaultRegistry, Methods, Registry } from "../registry";
|
||||
import type { shim } from "./mixin";
|
||||
|
||||
export interface Base<T> {
|
||||
export type Base<T> = T extends { [shim]: { value: infer U } }
|
||||
? { readonly value: U }
|
||||
: {
|
||||
readonly value: T;
|
||||
}
|
||||
};
|
||||
|
||||
export type Fluent<
|
||||
T,
|
||||
Reg extends Registry = DefaultRegistry,
|
||||
> = Base<T> & Methods<T, Reg> & { readonly __registry: Reg };
|
||||
> = Base<T> & Methods<T, Reg>;
|
||||
|
||||
export function makeFluent<const Reg extends Registry>(
|
||||
registry: Reg,
|
||||
) {
|
||||
const fluent = <T>(value: T) => {
|
||||
const fluent = <const T>(value: T) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const f = { value } as never as Fluent<T, Reg>;
|
||||
|
||||
|
||||
+20
-23
@@ -8,30 +8,19 @@ export interface Props {
|
||||
readonly meta: { registry: Registry };
|
||||
}
|
||||
|
||||
type UnionToIntersection<U> = (
|
||||
U extends unknown ? (x: U) => void : never
|
||||
) extends (x: infer I) => void
|
||||
? I
|
||||
: never;
|
||||
|
||||
type MixinHKT = HKT<Props>;
|
||||
type MixinFn<T extends MixinHKT> = (
|
||||
type MixinFn = (
|
||||
value: unknown,
|
||||
fluent: Partial<
|
||||
UnionToIntersection<
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
HKT.Apply<T, { value: any; meta: { registry: [] } }>
|
||||
>
|
||||
>,
|
||||
fluent: Record<PropertyKey, unknown>,
|
||||
callback: (value: unknown) => never,
|
||||
) => void;
|
||||
|
||||
export interface Mixin<T extends MixinHKT = MixinHKT> {
|
||||
hkt: T;
|
||||
fn: MixinFn<T>;
|
||||
fn: MixinFn;
|
||||
}
|
||||
|
||||
export function Mixin<T extends MixinHKT>(fn: MixinFn<T>): Mixin<T> {
|
||||
export function Mixin<T extends MixinHKT>(fn: MixinFn): Mixin<T> {
|
||||
return {
|
||||
hkt: never,
|
||||
fn,
|
||||
@@ -40,18 +29,26 @@ export function Mixin<T extends MixinHKT>(fn: MixinFn<T>): Mixin<T> {
|
||||
|
||||
export namespace Mixin {
|
||||
export type HKT = MixinHKT;
|
||||
export type Function<T extends MixinHKT> = MixinFn<T>;
|
||||
export type Function = MixinFn;
|
||||
}
|
||||
|
||||
// type Fluent<T, P extends Props> = _Fluent<
|
||||
// T,
|
||||
// P["meta"]["registry"]
|
||||
// > & {};
|
||||
export declare const shim: unique symbol;
|
||||
export interface Shim {
|
||||
input?: unknown;
|
||||
output?: HKT;
|
||||
value?: unknown;
|
||||
}
|
||||
|
||||
export type Input<P extends Props> = P["value"];
|
||||
export type Input<P extends Props> = P["value"] extends infer T
|
||||
? T extends { [shim]: { input: infer I } }
|
||||
? I
|
||||
: T
|
||||
: never;
|
||||
export type Return<T, P extends Props> = Fluent<
|
||||
T,
|
||||
P extends { meta: { registry: infer Reg } } ? Reg : never
|
||||
P["value"] extends { [shim]: { output: infer U extends HKT } }
|
||||
? HKT.Apply<U, T>
|
||||
: T,
|
||||
P["meta"]["registry"]
|
||||
>;
|
||||
|
||||
export type Instansiate<
|
||||
|
||||
+48
-2
@@ -9,5 +9,51 @@ export const never = undefined as never;
|
||||
|
||||
export type Constraint<T, U extends T> = U;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
export type Empty = Constraint<Record<PropertyKey, unknown>, {}>;
|
||||
export class AssertionError extends Error {
|
||||
public constructor(msg?: string) {
|
||||
super(msg);
|
||||
this.name = "AssertionError";
|
||||
}
|
||||
}
|
||||
|
||||
export function assert(
|
||||
condition: unknown,
|
||||
msg?: string,
|
||||
): asserts condition {
|
||||
if (!Boolean(condition))
|
||||
throw new AssertionError(
|
||||
["Assertion error", msg]
|
||||
.filter((v) => v !== undefined)
|
||||
.join(": "),
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters, @typescript-eslint/no-unused-vars
|
||||
export function assertType<T>(_v: unknown): asserts _v is T {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
type FunctionProto = Function;
|
||||
export interface HidePrototype {
|
||||
/** @deprecated */
|
||||
apply: FunctionProto["apply"];
|
||||
/** @deprecated */
|
||||
arguments: FunctionProto["arguments"];
|
||||
/** @deprecated */
|
||||
bind: FunctionProto["bind"];
|
||||
/** @deprecated */
|
||||
call: FunctionProto["call"];
|
||||
/** @deprecated */
|
||||
caller: FunctionProto["caller"];
|
||||
/** @deprecated */
|
||||
length: FunctionProto["length"];
|
||||
/** @deprecated */
|
||||
name: FunctionProto["name"];
|
||||
/** @deprecated */
|
||||
prototype: FunctionProto["prototype"];
|
||||
/** @deprecated */
|
||||
toString: FunctionProto["toString"];
|
||||
/** @deprecated */
|
||||
Symbol: SymbolConstructor;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,444 @@
|
||||
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";
|
||||
|
||||
type IterArgs<T extends readonly unknown[] = unknown[]> = [
|
||||
element: T[number],
|
||||
idx: number,
|
||||
array: Readonly<T>,
|
||||
];
|
||||
|
||||
type MaxDepth = 20;
|
||||
|
||||
type Unordered<T extends readonly unknown[]> = T extends unknown[]
|
||||
? T[number][]
|
||||
: readonly T[number][];
|
||||
|
||||
type Map<
|
||||
T extends readonly unknown[],
|
||||
U,
|
||||
TOut extends U[] = [],
|
||||
> = TOut["length"] extends MaxDepth
|
||||
? U[]
|
||||
: T extends readonly [infer T, ...infer TRest extends unknown[]]
|
||||
? Map<TRest, U, [...TOut, U]>
|
||||
: T extends readonly []
|
||||
? TOut
|
||||
: U[];
|
||||
|
||||
type Reverse<
|
||||
T extends readonly unknown[],
|
||||
TOut extends readonly T[number][] = [],
|
||||
> = TOut["length"] extends MaxDepth
|
||||
? T[number][]
|
||||
: T extends readonly [...infer TRest extends unknown[], infer T]
|
||||
? Reverse<TRest, [...TOut, T]>
|
||||
: T extends readonly []
|
||||
? TOut
|
||||
: T[number][];
|
||||
|
||||
namespace At {
|
||||
export type Dec<N extends number> = [
|
||||
-1,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
][N];
|
||||
|
||||
export type Select<
|
||||
T extends readonly unknown[],
|
||||
Skip extends number,
|
||||
> = Skip extends 0
|
||||
? T extends readonly [...unknown[], infer T]
|
||||
? T
|
||||
: undefined
|
||||
: T extends readonly [...infer TRest, unknown]
|
||||
? Select<TRest, Dec<Skip>>
|
||||
: undefined;
|
||||
}
|
||||
|
||||
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];
|
||||
|
||||
namespace Flat {
|
||||
type Inc<
|
||||
N extends number,
|
||||
Acc extends unknown[] = [],
|
||||
> = Acc["length"] extends N
|
||||
? [...Acc, null]["length"]
|
||||
: Inc<N, [...Acc, null]>;
|
||||
|
||||
export type Leaves<
|
||||
T,
|
||||
Depth extends number = 0,
|
||||
> = Depth extends MaxDepth
|
||||
? T
|
||||
: T extends readonly unknown[]
|
||||
? Leaves<T[number], Inc<Depth>>
|
||||
: T;
|
||||
|
||||
export type Rebuild<
|
||||
T extends readonly unknown[],
|
||||
Depth extends number = 0,
|
||||
> = Depth extends MaxDepth
|
||||
? T
|
||||
: T extends readonly [infer H, ...infer R]
|
||||
? H extends readonly unknown[]
|
||||
? [...Rebuild<H, Inc<Depth>>, ...Rebuild<R, Depth>]
|
||||
: [H, ...Rebuild<R, Depth>]
|
||||
: [];
|
||||
}
|
||||
|
||||
export type Flat<T extends readonly unknown[]> =
|
||||
number extends T["length"] ? Flat.Leaves<T>[] : Flat.Rebuild<T>;
|
||||
|
||||
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<this>) => Input<typeof t> extends infer T
|
||||
? T extends readonly (infer Item)[]
|
||||
? {
|
||||
at: <const K extends keyof T & number>(
|
||||
index: K,
|
||||
) => Return<At<T, K>, typeof t>;
|
||||
each: (
|
||||
callback: (...args: IterArgs<T>) => void,
|
||||
) => Return<T, typeof t>;
|
||||
map: <U>(
|
||||
callback: (...args: IterArgs<T>) => U,
|
||||
) => Return<Map<T, U>, typeof t>;
|
||||
filter: (
|
||||
callback: (...args: IterArgs<T>) => boolean,
|
||||
) => Return<Unordered<T>, typeof t>;
|
||||
reduce: (<U>(
|
||||
initial: U,
|
||||
callback: (
|
||||
value: U,
|
||||
...args: IterArgs<T>
|
||||
) => U,
|
||||
) => Return<U, typeof t>) & {
|
||||
right: <U>(
|
||||
initial: U,
|
||||
callback: (
|
||||
value: U,
|
||||
...args: IterArgs<T>
|
||||
) => U,
|
||||
) => Return<U, typeof t>;
|
||||
} & HidePrototype;
|
||||
length: () => Return<T["length"], typeof t>;
|
||||
sort: ((
|
||||
callback: (
|
||||
a: Item,
|
||||
b: Item,
|
||||
arr: Readonly<T>,
|
||||
) => number,
|
||||
) => Return<Unordered<T>, typeof t>) &
|
||||
([Item] extends [string]
|
||||
? {
|
||||
alpha: () => Return<
|
||||
Unordered<T>,
|
||||
typeof t
|
||||
>;
|
||||
}
|
||||
: {
|
||||
alpha: (
|
||||
via: (v: Item) => string,
|
||||
) => Return<
|
||||
Unordered<T>,
|
||||
typeof t
|
||||
>;
|
||||
}) &
|
||||
([Item] extends [number]
|
||||
? {
|
||||
ascending: () => Return<
|
||||
Unordered<T>,
|
||||
typeof t
|
||||
>;
|
||||
descending: () => Return<
|
||||
Unordered<T>,
|
||||
typeof t
|
||||
>;
|
||||
}
|
||||
: {
|
||||
ascending: (
|
||||
via: (v: Item) => number,
|
||||
) => Return<
|
||||
Unordered<T>,
|
||||
typeof t
|
||||
>;
|
||||
descending: (
|
||||
via: (v: Item) => number,
|
||||
) => Return<
|
||||
Unordered<T>,
|
||||
typeof t
|
||||
>;
|
||||
}) &
|
||||
HidePrototype;
|
||||
reverse: () => Return<Reverse<T>, typeof t>;
|
||||
}
|
||||
: unknown
|
||||
: never;
|
||||
}
|
||||
export const Array = Mixin<Array>((value, $, fluent) => {
|
||||
if (!isArray(value)) return;
|
||||
|
||||
$.at = (index: number) => {
|
||||
return fluent(value.at(index));
|
||||
};
|
||||
|
||||
$.each = (callback: (...args: IterArgs) => void) => {
|
||||
value.forEach(callback);
|
||||
return fluent(value);
|
||||
};
|
||||
|
||||
$.map = (callback: (...args: IterArgs) => unknown) => {
|
||||
return fluent(value.map(callback));
|
||||
};
|
||||
|
||||
$.filter = (callback: (...args: IterArgs) => boolean) => {
|
||||
return fluent(value.filter(callback));
|
||||
};
|
||||
|
||||
$.reduce = (
|
||||
initial: unknown,
|
||||
callback: (value: unknown, ...args: IterArgs) => unknown,
|
||||
) => {
|
||||
return fluent(value.reduce(callback, initial));
|
||||
};
|
||||
assertType<object>($.reduce);
|
||||
Object.assign($.reduce, {
|
||||
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<object>($.sort);
|
||||
Object.assign($.sort, {
|
||||
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)));
|
||||
},
|
||||
});
|
||||
|
||||
$.reverse = () => {
|
||||
return fluent(value.toReversed());
|
||||
};
|
||||
});
|
||||
|
||||
if (import.meta.vitest) {
|
||||
const { test, expect, vi } = import.meta.vitest;
|
||||
|
||||
const registry = [Array] as const;
|
||||
const $ = makeFluent(registry);
|
||||
|
||||
test("at()", () => {
|
||||
const arr = [1, 2, 3] as const;
|
||||
|
||||
expect($(arr).at(0).value).toBe(arr[0]);
|
||||
expect($(arr).at(5).value).toBe(undefined);
|
||||
|
||||
expect($(arr).at(-1).value).toBe(arr[arr.length - 1]);
|
||||
expect($(arr).at(-5).value).toBe(undefined);
|
||||
});
|
||||
|
||||
test("each()", () => {
|
||||
const arr = [5, 7, 1] as const;
|
||||
const callback = vi.fn();
|
||||
|
||||
const result = $(arr).each(callback).value;
|
||||
|
||||
expect(result).toMatchObject(arr);
|
||||
|
||||
for (let i = 0; i < arr.length; i++)
|
||||
expect(callback).toHaveBeenNthCalledWith(
|
||||
i + 1,
|
||||
arr[i],
|
||||
i,
|
||||
arr,
|
||||
);
|
||||
});
|
||||
|
||||
test("map()", () => {
|
||||
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),
|
||||
);
|
||||
|
||||
for (let i = 0; i < arr.length; i++)
|
||||
expect(isEven).toHaveBeenNthCalledWith(
|
||||
i + 1,
|
||||
arr[i],
|
||||
i,
|
||||
arr,
|
||||
);
|
||||
});
|
||||
|
||||
test("filter()", () => {
|
||||
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),
|
||||
);
|
||||
|
||||
for (let i = 0; i < arr.length; i++)
|
||||
expect(isEven).toHaveBeenNthCalledWith(
|
||||
i + 1,
|
||||
arr[i],
|
||||
i,
|
||||
arr,
|
||||
);
|
||||
});
|
||||
|
||||
test("reduce()", () => {
|
||||
const arr = [5, 5, 5, 5, 5] as const;
|
||||
|
||||
const callback = vi.fn((a: number, b: number) => a + b);
|
||||
const expected = arr.reduce(callback, 0);
|
||||
|
||||
callback.mockClear();
|
||||
|
||||
expect($(arr).reduce(0, callback).value).toBe(expected);
|
||||
|
||||
let sum = 0;
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
expect(callback).toHaveBeenNthCalledWith(
|
||||
i + 1,
|
||||
sum,
|
||||
arr[i],
|
||||
i,
|
||||
arr,
|
||||
);
|
||||
sum += arr[i]!;
|
||||
}
|
||||
|
||||
callback.mockClear();
|
||||
|
||||
expect($(arr).reduce.right(0, callback).value).toBe(expected);
|
||||
|
||||
sum = 0;
|
||||
for (let i = arr.length; i > 0; i--) {
|
||||
expect(callback).toHaveBeenNthCalledWith(
|
||||
arr.length - i + 1,
|
||||
sum,
|
||||
arr[i - 1],
|
||||
i - 1,
|
||||
arr,
|
||||
);
|
||||
sum += arr[i - 1]!;
|
||||
}
|
||||
});
|
||||
|
||||
test("length()", () => {
|
||||
const arr = [3, 3, 3] as const;
|
||||
|
||||
expect($(arr).length().value).toBe(arr.length);
|
||||
});
|
||||
|
||||
test("sort()", () => {
|
||||
const numbers = [1, 5, 3, 4, 2] as const;
|
||||
|
||||
const callback = (a: number) => (a % 2 === 0 ? -1 : 1);
|
||||
|
||||
expect($(numbers).sort(callback).value).toMatchObject(
|
||||
numbers.toSorted(callback),
|
||||
);
|
||||
|
||||
expect($(numbers).sort.ascending().value).toMatchObject(
|
||||
numbers.toSorted((a, b) => a - b),
|
||||
);
|
||||
expect($(numbers).sort.descending().value).toMatchObject(
|
||||
numbers.toSorted((a, b) => b - a),
|
||||
);
|
||||
|
||||
const boxed = [{ v: 3 }, { v: 2 }, { v: 1 }];
|
||||
expect(
|
||||
$(boxed).sort.ascending((v) => v.v).value,
|
||||
).toMatchObject(boxed.toSorted((a, b) => a.v - b.v));
|
||||
expect(
|
||||
$(boxed).sort.descending((v) => v.v).value,
|
||||
).toMatchObject(boxed.toSorted((a, b) => b.v - a.v));
|
||||
|
||||
const strings = [
|
||||
"beta",
|
||||
"alpha",
|
||||
"bart",
|
||||
"apple",
|
||||
"charlie",
|
||||
] as const;
|
||||
|
||||
expect($(strings).sort.alpha().value).toMatchObject(
|
||||
strings.toSorted(),
|
||||
);
|
||||
});
|
||||
}
|
||||
+105
-16
@@ -1,47 +1,136 @@
|
||||
import { makeFluent } from "../base";
|
||||
import type { HKT } from "../base/hkt";
|
||||
import {
|
||||
Mixin,
|
||||
type Input,
|
||||
type Props,
|
||||
type Return,
|
||||
} from "../base/mixin";
|
||||
import type { Empty } from "../internal";
|
||||
import type { Methods } from "../registry";
|
||||
import { Mixin, shim, type Input, type Return } from "../base/mixin";
|
||||
import { assert, assertType, never } from "../internal";
|
||||
import { Base } from "./base";
|
||||
|
||||
interface AwaitedIdentitity extends HKT {
|
||||
new: (t: HKT.T<this>) => Awaited<Promise<typeof t>>;
|
||||
}
|
||||
|
||||
class Awaited<T extends Promise<unknown>> {
|
||||
public value: T;
|
||||
public constructor(value: T) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public get [shim]() {
|
||||
return never as {
|
||||
input: T extends Promise<infer U> ? U : never;
|
||||
output: AwaitedIdentitity;
|
||||
value: T;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface AwaitedMixin extends Mixin.HKT {
|
||||
export interface AsyncMixin extends Mixin.HKT {
|
||||
new: (t: HKT.T<this>) => Input<typeof t> extends infer T
|
||||
? T extends Promise<unknown>
|
||||
? {
|
||||
readonly awaited: Return<Awaited<T>, typeof t>;
|
||||
then: <U>(
|
||||
callback: (
|
||||
value: T extends Promise<infer T>
|
||||
? T
|
||||
: never,
|
||||
) => U,
|
||||
) => Return<Promise<U>, typeof t>;
|
||||
}
|
||||
: 0
|
||||
: unknown
|
||||
: never;
|
||||
}
|
||||
|
||||
export const AwaitedMixin = Mixin<AwaitedMixin>(
|
||||
(value, $, fluent) => {},
|
||||
interface BaseFluent<T> {
|
||||
value: T;
|
||||
[K: PropertyKey]: unknown;
|
||||
}
|
||||
|
||||
export const AsyncMixin = Mixin<AsyncMixin>((value, $, fluent) => {
|
||||
if (!(value instanceof Promise)) return;
|
||||
|
||||
$.then = (callback: (value: unknown) => unknown) => {
|
||||
return fluent(value.then(callback));
|
||||
};
|
||||
|
||||
Object.defineProperty($, "awaited", {
|
||||
enumerable: true,
|
||||
get() {
|
||||
let v: Promise<BaseFluent<unknown>> = 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<Record<PropertyKey, unknown>>(
|
||||
obj,
|
||||
);
|
||||
|
||||
obj = obj[node];
|
||||
}
|
||||
|
||||
assert(typeof obj === "function");
|
||||
assertType<
|
||||
(
|
||||
...args: unknown[]
|
||||
) => BaseFluent<unknown>
|
||||
>(obj);
|
||||
|
||||
return obj(...args);
|
||||
});
|
||||
return target.apply(thisArg, args);
|
||||
},
|
||||
});
|
||||
|
||||
return proxy;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
if (import.meta.vitest) {
|
||||
const { test, expect } = import.meta.vitest;
|
||||
|
||||
const registry = [Base, AwaitedMixin] as const;
|
||||
const registry = [Base, AsyncMixin] as const;
|
||||
const $ = makeFluent(registry);
|
||||
|
||||
test(".awaited", () => {
|
||||
test(".awaited", async () => {
|
||||
const value = 10;
|
||||
const promise = new Promise<number>((r) => {
|
||||
r(10);
|
||||
r(value);
|
||||
});
|
||||
|
||||
const m = $(promise).V;
|
||||
const increment = (n: number) => n + 1;
|
||||
|
||||
const result = $(promise).awaited.transform(increment).value;
|
||||
|
||||
expect(await result).toBe(increment(value));
|
||||
});
|
||||
|
||||
test("then()", async () => {
|
||||
const value = 10;
|
||||
const promise = new Promise<number>((r) => {
|
||||
r(value);
|
||||
});
|
||||
|
||||
const increment = (n: number) => n + 1;
|
||||
|
||||
const result = $(promise).then(increment).value;
|
||||
|
||||
expect(await result).toBe(increment(value));
|
||||
});
|
||||
}
|
||||
|
||||
+2
-4
@@ -1,7 +1,6 @@
|
||||
import { makeFluent } from "../base";
|
||||
import type { HKT } from "../base/hkt";
|
||||
import { Mixin, type Input, type Return } from "../base/mixin";
|
||||
import type { Empty } from "../internal";
|
||||
|
||||
export interface Base extends Mixin.HKT {
|
||||
new: (t: HKT.T<this>) => Input<typeof t> extends infer T
|
||||
@@ -12,18 +11,17 @@ export interface Base extends Mixin.HKT {
|
||||
transform<U>(
|
||||
callback: (value: T) => U,
|
||||
): Return<U, typeof t>;
|
||||
readonly V: Return<number, typeof t>;
|
||||
}
|
||||
: never;
|
||||
}
|
||||
|
||||
export const Base = Mixin<Base>((value, $, fluent) => {
|
||||
$.tap = (callback) => {
|
||||
$.tap = (callback: (value: unknown) => void) => {
|
||||
callback(value);
|
||||
return fluent(value);
|
||||
};
|
||||
|
||||
$.transform = (callback) => {
|
||||
$.transform = (callback: (value: unknown) => unknown) => {
|
||||
return fluent(callback(value));
|
||||
};
|
||||
});
|
||||
|
||||
+2
-1
@@ -1,2 +1,3 @@
|
||||
export { Base } from "./base";
|
||||
export { AwaitedMixin as Awaited } from "./awaited";
|
||||
export { AsyncMixin as Async } from "./awaited";
|
||||
export { Optional } from "./optional";
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { makeFluent } from "../base";
|
||||
import type { HKT } from "../base/hkt";
|
||||
import {
|
||||
Mixin,
|
||||
type Input,
|
||||
type Props,
|
||||
type Return,
|
||||
} from "../base/mixin";
|
||||
import { assertType, type HidePrototype } from "../internal";
|
||||
|
||||
type NoneSentinel = null | undefined;
|
||||
type None<T> = Extract<T, NoneSentinel>;
|
||||
type Some<T> = Exclude<T, NoneSentinel>;
|
||||
|
||||
function isNone<T>(v: T): v is None<T> {
|
||||
return v === null || v === undefined;
|
||||
}
|
||||
|
||||
interface Where<T, t extends Props> {
|
||||
where: <U = null>(
|
||||
callback: (v: T) => boolean,
|
||||
fallback?: U,
|
||||
) => Return<T | U, t>;
|
||||
}
|
||||
|
||||
export interface Optional extends Mixin.HKT {
|
||||
new: (t: HKT.T<this>) => Input<typeof t> extends infer T
|
||||
? None<T> extends never
|
||||
? Where<T, typeof t>
|
||||
: {
|
||||
and: <U>(
|
||||
callback: (v: Some<T>) => U,
|
||||
) => Return<None<T> | U, typeof t>;
|
||||
or: (<U>(
|
||||
fallback: U,
|
||||
) => Return<Some<T> | U, typeof t>) & {
|
||||
else: <U>(
|
||||
callback: (v: None<T>) => U,
|
||||
) => Return<Some<T> | U, typeof t>;
|
||||
} & HidePrototype;
|
||||
} & Where<T, typeof t>
|
||||
: never;
|
||||
}
|
||||
|
||||
export const Optional = Mixin<Optional>((value, $, fluent) => {
|
||||
if (isNone(value)) {
|
||||
$.and = () => {
|
||||
return fluent(value);
|
||||
};
|
||||
|
||||
$.or = (fallback: unknown) => {
|
||||
return fluent(fallback);
|
||||
};
|
||||
|
||||
assertType<object>($.or);
|
||||
Object.assign($.or, {
|
||||
else: (callback: (v: unknown) => unknown) => {
|
||||
return fluent(callback(value));
|
||||
},
|
||||
});
|
||||
} else {
|
||||
$.and = (callback: (v: unknown) => unknown) => {
|
||||
return fluent(callback(value));
|
||||
};
|
||||
|
||||
$.or = () => {
|
||||
return fluent(value);
|
||||
};
|
||||
|
||||
assertType<object>($.or);
|
||||
Object.assign($.or, {
|
||||
else: () => {
|
||||
return fluent(value);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
$.where = (
|
||||
callback: (v: unknown) => boolean,
|
||||
fallback = null,
|
||||
) => {
|
||||
if (callback(value)) return fluent(value);
|
||||
return fluent(fallback);
|
||||
};
|
||||
});
|
||||
|
||||
if (import.meta.vitest) {
|
||||
const { test, expect } = import.meta.vitest;
|
||||
|
||||
const registry = [Optional] as const;
|
||||
const $ = makeFluent(registry);
|
||||
|
||||
type Value = number | null;
|
||||
|
||||
test("and()", () => {
|
||||
const value = 10;
|
||||
const some = value as Value;
|
||||
const none = null as Value;
|
||||
|
||||
const callback = (v: number) => v + 5;
|
||||
|
||||
expect($(some).and(callback).value).toBe(callback(value));
|
||||
expect($(none).and(callback).value).toBe(null);
|
||||
});
|
||||
|
||||
test("or()", () => {
|
||||
const some = 10 as Value;
|
||||
const none = null as Value;
|
||||
|
||||
const fallback = 15 as const;
|
||||
|
||||
expect($(some).or(fallback).value).toBe(some);
|
||||
expect($(none).or(fallback).value).toBe(fallback);
|
||||
|
||||
const callback = () => fallback;
|
||||
|
||||
expect($(some).or.else(callback).value).toBe(some);
|
||||
expect($(none).or.else(callback).value).toBe(fallback);
|
||||
});
|
||||
|
||||
test("where()", () => {
|
||||
const even = 4;
|
||||
const odd = 7;
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
+7
-8
@@ -1,26 +1,25 @@
|
||||
import { Mixin, type Instansiate } from "./base/mixin";
|
||||
import type { Empty } from "./internal";
|
||||
import { Base, Awaited } from "./mixin";
|
||||
import { Async, Base, Optional } from "./mixin";
|
||||
import { Array } from "./mixin/array";
|
||||
|
||||
export type Registry = readonly Mixin[];
|
||||
|
||||
export const DEFAULT_REGISTRY = [
|
||||
Async,
|
||||
Array,
|
||||
Base,
|
||||
Awaited,
|
||||
Optional,
|
||||
] as const satisfies Registry;
|
||||
export type DefaultRegistry = typeof DEFAULT_REGISTRY;
|
||||
|
||||
type Expand<T> = { [K in keyof T]: T[K] };
|
||||
type Merge<T, U> = [T] extends [0] ? U : [U] extends [0] ? T : T & U;
|
||||
|
||||
export type Methods<
|
||||
T,
|
||||
Reg extends Registry,
|
||||
TReg extends Registry = Reg,
|
||||
TOut = {},
|
||||
TOut extends Record<PropertyKey, unknown> = Record<never, never>,
|
||||
> = TReg extends readonly [
|
||||
infer M extends Mixin,
|
||||
...infer Rest extends Registry,
|
||||
]
|
||||
? Methods<T, Reg, Rest, Merge<TOut, Instansiate<M, T, Reg>>>
|
||||
? Methods<T, Reg, Rest, TOut & Instansiate<M, T, Reg>>
|
||||
: TOut;
|
||||
|
||||
Reference in New Issue
Block a user