This commit is contained in:
2026-06-23 01:13:03 +02:00
parent a3c85a7983
commit 3dc22e3f62
13 changed files with 1210 additions and 10 deletions
+1
View File
@@ -45,6 +45,7 @@ export default defineConfig([
reportUsedIgnorePattern: true,
},
],
"@typescript-eslint/no-namespace": "off",
},
languageOptions: {
parserOptions: {
+1
View File
@@ -17,6 +17,7 @@
"esbuild": "^0.28.1",
"eslint": "^10.3.0",
"jiti": "^2.7.0",
"madge": "^8.0.0",
"prettier": "^3.8.4",
"typescript": "~6.0.2",
"typescript-eslint": "^8.59.2",
+948
View File
File diff suppressed because it is too large Load Diff
+20
View File
@@ -0,0 +1,20 @@
export interface HKT<In = unknown, Out = unknown> {
readonly _meta: In;
readonly _t: In;
new: (t: never) => Out;
}
type Param<
This extends HKT,
U = This["_meta"],
> = This["_t"] extends infer T ? (T extends U ? T : U) : never;
export namespace HKT {
export type T<
This extends HKT,
T = This extends { _t: infer I } ? I : unknown,
> = Param<This, T>;
export type Apply<T extends HKT, t extends T["_t"]> = ReturnType<
(T & { _t: t })["new"]
>;
}
+28
View File
@@ -0,0 +1,28 @@
import type { DefaultRegistry, Methods, Registry } from "../registry";
export interface Base<T> {
readonly value: T;
}
export type Fluent<
T,
Reg extends Registry = DefaultRegistry,
> = Base<T> & Methods<T, Reg> & { readonly __registry: Reg };
export function makeFluent<const Reg extends Registry>(
registry: Reg,
) {
const fluent = <T>(value: T) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const f = { value } as never as Fluent<T, Reg>;
for (const mixin of registry) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
mixin.fn(value, f, fluent as (value: unknown) => never);
}
return f;
};
return fluent;
}
+61
View File
@@ -0,0 +1,61 @@
import type { Fluent } from ".";
import { never } from "../internal";
import type { Registry } from "../registry";
import type { HKT } from "./hkt";
export interface Props {
readonly value: unknown;
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> = (
value: unknown,
fluent: Partial<
UnionToIntersection<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
HKT.Apply<T, { value: any; meta: { registry: [] } }>
>
>,
callback: (value: unknown) => never,
) => void;
export interface Mixin<T extends MixinHKT = MixinHKT> {
hkt: T;
fn: MixinFn<T>;
}
export function Mixin<T extends MixinHKT>(fn: MixinFn<T>): Mixin<T> {
return {
hkt: never,
fn,
};
}
export namespace Mixin {
export type HKT = MixinHKT;
export type Function<T extends MixinHKT> = MixinFn<T>;
}
// type Fluent<T, P extends Props> = _Fluent<
// T,
// P["meta"]["registry"]
// > & {};
export type Input<P extends Props> = P["value"];
export type Return<T, P extends Props> = Fluent<
T,
P extends { meta: { registry: infer Reg } } ? Reg : never
>;
export type Instansiate<
M extends Mixin,
T,
Reg extends Registry,
> = HKT.Apply<M["hkt"], { value: T; meta: { registry: Reg } }>;
+3 -9
View File
@@ -1,10 +1,4 @@
function add(a: number, b: number): number {
return a + b;
}
import { makeFluent } from "./base";
import { DEFAULT_REGISTRY } from "./registry";
if (import.meta.vitest) {
const { it, expect } = import.meta.vitest;
it("adds", () => {
expect(add(1, 2)).toBe(3);
});
}
export const fluent = makeFluent(DEFAULT_REGISTRY);
+13
View File
@@ -0,0 +1,13 @@
import type { HKT } from "./base/hkt";
export interface Identity extends HKT {
new: (t: HKT.T<this>) => typeof t;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
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>, {}>;
+47
View File
@@ -0,0 +1,47 @@
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 { Base } from "./base";
class Awaited<T extends Promise<unknown>> {
public value: T;
public constructor(value: T) {
this.value = value;
}
}
export interface AwaitedMixin 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>;
}
: 0
: never;
}
export const AwaitedMixin = Mixin<AwaitedMixin>(
(value, $, fluent) => {},
);
if (import.meta.vitest) {
const { test, expect } = import.meta.vitest;
const registry = [Base, AwaitedMixin] as const;
const $ = makeFluent(registry);
test(".awaited", () => {
const promise = new Promise<number>((r) => {
r(10);
});
const m = $(promise).V;
});
}
+59
View File
@@ -0,0 +1,59 @@
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
? {
tap(
callback: (value: T) => void,
): Return<T, typeof t>;
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) => {
callback(value);
return fluent(value);
};
$.transform = (callback) => {
return fluent(callback(value));
};
});
if (import.meta.vitest) {
const { test, expect } = import.meta.vitest;
const registry = [Base] as const;
const $ = makeFluent(registry);
test("tap()", () => {
const value = 5;
let out = 0;
const result = $(value).tap((v) => (out = v)).value;
expect(result).toBe(value);
expect(out).toBe(value);
});
test("transform()", () => {
const value = 5;
const increment = (v: number) => v + 1;
expect($(value).transform(increment).value).toBe(
increment(value),
);
expect(
$(value).transform(increment).transform(increment).value,
).toBe(increment(increment(value)));
});
}
+2
View File
@@ -0,0 +1,2 @@
export { Base } from "./base";
export { AwaitedMixin as Awaited } from "./awaited";
+26
View File
@@ -0,0 +1,26 @@
import { Mixin, type Instansiate } from "./base/mixin";
import type { Empty } from "./internal";
import { Base, Awaited } from "./mixin";
export type Registry = readonly Mixin[];
export const DEFAULT_REGISTRY = [
Base,
Awaited,
] 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 = {},
> = TReg extends readonly [
infer M extends Mixin,
...infer Rest extends Registry,
]
? Methods<T, Reg, Rest, Merge<TOut, Instansiate<M, T, Reg>>>
: TOut;
+1 -1
View File
@@ -4,7 +4,7 @@
"target": "es2023",
"lib": ["ES2023"],
"module": "esnext",
"types": ["vitest/importMeta"],
"types": ["node", "vitest/importMeta"],
"skipLibCheck": true,
/* Bundler mode */