| 1 | /** |
| 2 | * Controls how values appear in logs and debugging output. |
| 3 | * |
| 4 | * Effect data types use `Inspectable` to provide stable string, JSON, and |
| 5 | * Node.js inspection output. This keeps custom values readable in logs, REPLs, |
| 6 | * test failures, and diagnostics. This module defines the Node inspect symbol, |
| 7 | * the `Inspectable` interface, safe conversion helpers, and shared prototype or |
| 8 | * class implementations for custom values. |
| 9 | * |
| 10 | * @since 2.0.0 |
| 11 | */ |
| 12 | import { format, formatJson } from "./Formatter.ts" |
| 13 | import * as Predicate from "./Predicate.ts" |
| 14 | import { redact } from "./Redactable.ts" |
| 15 | |
| 16 | /** |
| 17 | * Defines the symbol used by Node.js for custom object inspection. |
| 18 | * |
| 19 | * **When to use** |
| 20 | * |
| 21 | * Use to implement Node.js custom inspection for a value. |
| 22 | * |
| 23 | * **Details** |
| 24 | * |
| 25 | * This symbol is recognized by Node.js's `util.inspect()` function and the REPL |
| 26 | * for custom object representation. When an object has a method with this symbol, |
| 27 | * it will be called to determine how the object should be displayed. |
| 28 | * |
| 29 | * **Example** (Defining custom Node inspection) |
| 30 | * |
| 31 | * ```ts import.meta.vitest |
| 32 | * import { Inspectable } from "effect" |
| 33 | * |
| 34 | * class CustomObject { |
| 35 | * constructor(private value: string) {} |
| 36 | * |
| 37 | * [Inspectable.NodeInspectSymbol]() { |
| 38 | * return `CustomObject(${this.value})` |
| 39 | * } |
| 40 | * } |
| 41 | * |
| 42 | * const obj = new CustomObject("hello") |
| 43 | * obj[Inspectable.NodeInspectSymbol]() // => "CustomObject(hello)" |
| 44 | * ``` |
| 45 | * |
| 46 | * @category symbols |
| 47 | * @since 2.0.0 |
| 48 | */ |
| 49 | export const NodeInspectSymbol = Symbol.for("nodejs.util.inspect.custom") |
| 50 | |
| 51 | /** |
| 52 | * The type of the Node.js inspection symbol used for custom object inspection. |
| 53 | * This symbol type is used to implement custom inspection behavior in Node.js |
| 54 | * environments. |
| 55 | * |
| 56 | * **When to use** |
| 57 | * |
| 58 | * Use to type methods keyed by the Node.js custom inspection symbol. |
| 59 | * |
| 60 | * **Example** (Typing custom Node inspection) |
| 61 | * |
| 62 | * ```ts import.meta.vitest |
| 63 | * import { Inspectable } from "effect" |
| 64 | * |
| 65 | * class CustomObject { |
| 66 | * constructor(private value: string) {} |
| 67 | * |
| 68 | * [Inspectable.NodeInspectSymbol]() { |
| 69 | * return `CustomObject(${this.value})` |
| 70 | * } |
| 71 | * } |
| 72 | * |
| 73 | * const obj = new CustomObject("test") |
| 74 | * obj[Inspectable.NodeInspectSymbol]() // => "CustomObject(test)" |
| 75 | * ``` |
| 76 | * |
| 77 | * @category symbols |
| 78 | * @since 2.0.0 |
| 79 | */ |
| 80 | export type NodeInspectSymbol = typeof NodeInspectSymbol |
| 81 | |
| 82 | /** |
| 83 | * Interface for objects that can be inspected and provide custom string representations. |
| 84 | * |
| 85 | * **When to use** |
| 86 | * |
| 87 | * Use to define values with custom string, JSON, and Node.js inspection output. |
| 88 | * |
| 89 | * **Details** |
| 90 | * |
| 91 | * Objects implementing this interface can control how they appear in debugging contexts, |
| 92 | * JSON serialization, and Node.js inspection. This is particularly useful for creating |
| 93 | * custom data types that display meaningful information during development. |
| 94 | * |
| 95 | * **Example** (Implementing inspectable objects) |
| 96 | * |
| 97 | * ```ts import.meta.vitest |
| 98 | * import { Formatter, Inspectable } from "effect" |
| 99 | * |
| 100 | * class Result implements Inspectable.Inspectable { |
| 101 | * constructor( |
| 102 | * private readonly tag: "Success" | "Failure", |
| 103 | * private readonly value: unknown |
| 104 | * ) {} |
| 105 | * |
| 106 | * toString(): string { |
| 107 | * return Formatter.format(this.toJSON()) |
| 108 | * } |
| 109 | * |
| 110 | * toJSON() { |
| 111 | * return { _tag: this.tag, value: this.value } |
| 112 | * } |
| 113 | * |
| 114 | * [Inspectable.NodeInspectSymbol]() { |
| 115 | * return this.toJSON() |
| 116 | * } |
| 117 | * } |
| 118 | * |
| 119 | * const success = new Result("Success", 42) |
| 120 | * success.toString() // => "{\"_tag\":\"Success\",\"value\":42}" |
| 121 | * ``` |
| 122 | * |
| 123 | * @category models |
| 124 | * @since 2.0.0 |
| 125 | */ |
| 126 | export interface Inspectable { |
| 127 | toString(): string |
| 128 | toJSON(): unknown |
| 129 | [NodeInspectSymbol](): unknown |
| 130 | } |
| 131 | |
| 132 | /** |
| 133 | * Converts a value to its structured inspection representation. |
| 134 | * |
| 135 | * **When to use** |
| 136 | * |
| 137 | * Use when you need the structured representation of an inspectable value |
| 138 | * without risking unhandled errors. |
| 139 | * |
| 140 | * **Details** |
| 141 | * |
| 142 | * This function applies redaction before extracting data from objects that |
| 143 | * implement `toJSON`, recursively processes arrays, and handles errors |
| 144 | * gracefully. Plain objects are returned unchanged, so the result is not |
| 145 | * guaranteed to be accepted by `JSON.stringify`; it may still contain values |
| 146 | * such as `BigInt`, functions, or circular references. |
| 147 | * |
| 148 | * @see {@link toStringUnknown} for converting unknown values to strings |
| 149 | * |
| 150 | * @category converting |
| 151 | * @since 4.0.0 |
| 152 | */ |
| 153 | export const toJson = (input: unknown): unknown => { |
| 154 | try { |
| 155 | input = redact(input) |
| 156 | if ( |
| 157 | Predicate.hasProperty(input, "toJSON") && |
| 158 | Predicate.isFunction(input["toJSON"]) && |
| 159 | input["toJSON"].length === 0 |
| 160 | ) { |
| 161 | return input.toJSON() |
| 162 | } else if (Array.isArray(input)) { |
| 163 | return input.map(toJson) |
| 164 | } |
| 165 | return input |
| 166 | } catch { |
| 167 | return "[toJSON threw]" |
| 168 | } |
| 169 | } |
| 170 | |
| 171 | /** |
| 172 | * Converts an unknown value to a string for diagnostics. |
| 173 | * |
| 174 | * **When to use** |
| 175 | * |
| 176 | * Use to produce a diagnostic string from a value whose runtime type is unknown. |
| 177 | * |
| 178 | * **Details** |
| 179 | * |
| 180 | * Strings are returned unchanged. Objects are formatted as JSON using the |
| 181 | * provided whitespace setting when possible, and values that cannot be |
| 182 | * formatted are converted with `String`. |
| 183 | * |
| 184 | * @category converting |
| 185 | * @since 2.0.0 |
| 186 | */ |
| 187 | export const toStringUnknown = (u: unknown, whitespace: number | string | undefined = 2): string => { |
| 188 | if (typeof u === "string") { |
| 189 | return u |
| 190 | } |
| 191 | try { |
| 192 | return typeof u === "object" ? formatJson(u, { space: whitespace }) : format(u, { space: whitespace }) |
| 193 | } catch { |
| 194 | return String(u) |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | /** |
| 199 | * A base prototype object that implements the {@link Inspectable} interface. |
| 200 | * |
| 201 | * **When to use** |
| 202 | * |
| 203 | * Use as a prototype for plain objects that should share standard inspectable behavior. |
| 204 | * |
| 205 | * **Details** |
| 206 | * |
| 207 | * This object provides default implementations for the {@link Inspectable} methods. |
| 208 | * It can be used as a prototype for objects that want to be inspectable, |
| 209 | * or as a mixin to add inspection capabilities to existing objects. |
| 210 | * |
| 211 | * **Example** (Using the base inspectable prototype) |
| 212 | * |
| 213 | * ```ts import.meta.vitest |
| 214 | * import { Inspectable } from "effect" |
| 215 | * |
| 216 | * // Use as prototype |
| 217 | * const myObject = Object.create(Inspectable.BaseProto) |
| 218 | * myObject.name = "example" |
| 219 | * myObject.value = 42 |
| 220 | * |
| 221 | * myObject.toString() // => "\"[toJSON threw]\"" |
| 222 | * |
| 223 | * // Or extend in a constructor |
| 224 | * function MyClass(this: any, name: string) { |
| 225 | * this.name = name |
| 226 | * } |
| 227 | * MyClass.prototype = Object.create(Inspectable.BaseProto) |
| 228 | * MyClass.prototype.constructor = MyClass |
| 229 | * ``` |
| 230 | * |
| 231 | * @category prototypes |
| 232 | * @since 2.0.0 |
| 233 | */ |
| 234 | export const BaseProto: Inspectable = { |
| 235 | toJSON() { |
| 236 | return toJson(this) |
| 237 | }, |
| 238 | [NodeInspectSymbol]() { |
| 239 | return this.toJSON() |
| 240 | }, |
| 241 | toString() { |
| 242 | return format(this.toJSON()) |
| 243 | } |
| 244 | } |
| 245 | |
| 246 | /** |
| 247 | * Provides an abstract base class that implements the Inspectable interface. |
| 248 | * |
| 249 | * **When to use** |
| 250 | * |
| 251 | * Use as a base class for inspectable objects that define their own JSON representation. |
| 252 | * |
| 253 | * **Details** |
| 254 | * |
| 255 | * This class provides a convenient way to create inspectable objects by extending it. |
| 256 | * Subclasses only need to implement the `toJSON()` method, and they automatically |
| 257 | * get proper `toString()` and Node.js inspection support. |
| 258 | * |
| 259 | * **Example** (Extending the inspectable base class) |
| 260 | * |
| 261 | * ```ts import.meta.vitest |
| 262 | * import { Inspectable } from "effect" |
| 263 | * |
| 264 | * class User extends Inspectable.Class { |
| 265 | * constructor( |
| 266 | * public readonly id: number, |
| 267 | * public readonly name: string, |
| 268 | * public readonly email: string |
| 269 | * ) { |
| 270 | * super() |
| 271 | * } |
| 272 | * |
| 273 | * toJSON() { |
| 274 | * return { |
| 275 | * _tag: "User", |
| 276 | * id: this.id, |
| 277 | * name: this.name, |
| 278 | * email: this.email |
| 279 | * } |
| 280 | * } |
| 281 | * } |
| 282 | * |
| 283 | * const user = new User(1, "Alice", "alice@example.com") |
| 284 | * user.toString() // => "{\"_tag\":\"User\",\"id\":1,\"name\":\"Alice\",\"email\":\"alice@example.com\"}" |
| 285 | * user[Inspectable.NodeInspectSymbol]() // => { _tag: "User", id: 1, name: "Alice", email: "alice@example.com" } |
| 286 | * ``` |
| 287 | * |
| 288 | * @category models |
| 289 | * @since 2.0.0 |
| 290 | */ |
| 291 | export abstract class Class { |
| 292 | /** |
| 293 | * Returns a JSON representation of this object. |
| 294 | * |
| 295 | * **When to use** |
| 296 | * |
| 297 | * Use to provide the JSON representation consumed by inherited inspection methods. |
| 298 | * |
| 299 | * **Details** |
| 300 | * |
| 301 | * Subclasses must implement this method to define how the object |
| 302 | * should be serialized for debugging and inspection purposes. |
| 303 | * |
| 304 | * @since 2.0.0 |
| 305 | */ |
| 306 | abstract toJSON(): unknown |
| 307 | /** |
| 308 | * Node.js custom inspection method. |
| 309 | * |
| 310 | * **When to use** |
| 311 | * |
| 312 | * Use to expose the class JSON representation to Node.js inspection. |
| 313 | * |
| 314 | * @since 2.0.0 |
| 315 | */ |
| 316 | [NodeInspectSymbol]() { |
| 317 | return this.toJSON() |
| 318 | } |
| 319 | /** |
| 320 | * Returns a formatted string representation of this object. |
| 321 | * |
| 322 | * **When to use** |
| 323 | * |
| 324 | * Use to format the class JSON representation as a string. |
| 325 | * |
| 326 | * @since 2.0.0 |
| 327 | */ |
| 328 | toString() { |
| 329 | return format(this.toJSON()) |
| 330 | } |
| 331 | } |