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)
+ })
})