diff --git a/.changeset/kind-poems-cough.md b/.changeset/kind-poems-cough.md new file mode 100644 index 00000000000..80f534b48a0 --- /dev/null +++ b/.changeset/kind-poems-cough.md @@ -0,0 +1,35 @@ +--- +"effect": minor +--- + +Simplified the creation of pipeable classes. + +```ts +class MyClass extends Pipeable.Class() { + constructor(public a: number) { + super() + } + methodA() { + return this.a + } +} +console.log(new MyClass(2).pipe((x) => x.methodA())) // 2 +``` + +```ts +class A { + constructor(public a: number) {} + methodA() { + return this.a + } +} +class B extends Pipeable.Class(A) { + constructor(private b: string) { + super(b.length) + } + methodB() { + return [this.b, this.methodA()] + } +} +console.log(new B("pipe").pipe((x) => x.methodB())) // ['pipe', 4] +``` diff --git a/packages/effect/src/Pipeable.ts b/packages/effect/src/Pipeable.ts index c41c7428b0f..2b54e6c9657 100644 --- a/packages/effect/src/Pipeable.ts +++ b/packages/effect/src/Pipeable.ts @@ -2,9 +2,11 @@ * @since 2.0.0 */ +import type { Ctor } from "./Types.js" + /** * @since 2.0.0 - * @category models + * @category Models */ export interface Pipeable { pipe(this: A): A @@ -522,3 +524,43 @@ export const pipeArguments = (self: A, args: IArguments): unknown => { } } } + +/** + * @since 3.15.0 + * @category Models + */ +export interface PipeableConstructor { + new(...args: Array): Pipeable +} + +/** + * @since 3.15.0 + * @category Prototypes + */ +export const Prototype: Pipeable = { + pipe() { + return pipeArguments(this, arguments) + } +} + +const Base: PipeableConstructor = (function() { + function PipeableBase() {} + PipeableBase.prototype = Prototype + return PipeableBase as any +})() + +/** + * @since 3.15.0 + * @category Constructors + */ +export const Class: { + (): PipeableConstructor + (klass: TBase): TBase & PipeableConstructor +} = (klass?: Ctor) => + klass ? + class extends klass { + pipe() { + return pipeArguments(this, arguments) + } + } + : Base diff --git a/packages/effect/src/Types.ts b/packages/effect/src/Types.ts index 0a989174d37..3e17e662be8 100644 --- a/packages/effect/src/Types.ts +++ b/packages/effect/src/Types.ts @@ -336,3 +336,8 @@ export type NotFunction = T extends Function ? never : T * @since 3.9.0 */ export type NoExcessProperties = T & { readonly [K in Exclude]: never } + +/** + * @since 3.15.0 + */ +export type Ctor = new(...args: Array) => T diff --git a/packages/effect/test/Pipeable.test.ts b/packages/effect/test/Pipeable.test.ts index ccf33d25ded..4fd08b06587 100644 --- a/packages/effect/test/Pipeable.test.ts +++ b/packages/effect/test/Pipeable.test.ts @@ -1,6 +1,6 @@ import { describe, it } from "@effect/vitest" -import { assertSome } from "@effect/vitest/utils" -import { Option } from "effect" +import { assertInstanceOf, assertSome, deepStrictEqual } from "@effect/vitest/utils" +import { Option, Pipeable } from "effect" describe("Pipeable", () => { it("pipeArguments", () => { @@ -70,4 +70,42 @@ describe("Pipeable", () => { 126 ) }) + it("pipeable", () => { + class A { + constructor(public a: number) {} + methodA() { + return this.a + } + } + class B extends Pipeable.Class(A) { + constructor(private b: string) { + super(b.length) + } + methodB() { + return [this.b, this.methodA()] + } + } + const b = new B("bb") + + assertInstanceOf(b, A) + assertInstanceOf(b, B) + deepStrictEqual(b.methodB(), ["bb", 2]) + deepStrictEqual(b.pipe((x) => x.methodB()), ["bb", 2]) + }) + it("Class", () => { + class A extends Pipeable.Class() { + constructor(public a: number) { + super() + } + methodA() { + return this.a + } + } + const a = new A(2) + + assertInstanceOf(a, A) + assertInstanceOf(a, Pipeable.Class()) + deepStrictEqual(a.methodA(), 2) + deepStrictEqual(a.pipe((x) => x.methodA()), 2) + }) })