Skip to content

Path

The Path service builds and inspects file system paths without hard-coding separators or assumptions about the host OS. By depending on the abstract Path service and providing a platform implementation (NodePath.layer on Node.js), the same code produces POSIX-style paths on Linux and macOS and Windows-style paths on Windows. Most methods are pure string operations; the two that can fail (fromFileUrl and toFileUrl) return an Effect with a BadArgument error.

import { NodePath } from "@effect/platform-node"
import { Effect, Path } from "effect"
const program = Effect.gen(function*() {
const path = yield* Path.Path
// `join` concatenates segments using the platform separator and collapses
// redundant slashes — never build paths with string templates.
const config = path.join("home", "ada", "project", "effect.config.ts")
// "home/ada/project/effect.config.ts" on POSIX
// `resolve` produces an absolute path, resolving against the current
// working directory and processing "." and ".." segments.
const absolute = path.resolve("project", "..", "shared", "util.ts")
// Decompose a path into its parts.
yield* Effect.log(path.basename(config)) // "effect.config.ts"
yield* Effect.log(path.dirname(config)) // "home/ada/project"
yield* Effect.log(path.extname(config)) // ".ts"
yield* Effect.log(`${config}\n${absolute}`)
}).pipe(
// Provide the Node implementation of the Path service.
Effect.provide(NodePath.layer)
)

parse turns a path string into a structured Path.Parsed object, and format rebuilds a path from such an object. This pair is the cleanest way to change one component of a path — for example, swapping a file extension:

import { Effect, Path } from "effect"
// Replace the extension of a path, e.g. ".ts" -> ".js".
const changeExtension = Effect.fn("changeExtension")(function*(
file: string,
ext: string
) {
const path = yield* Path.Path
const parsed = path.parse(file)
// `parsed` is { root, dir, base, ext, name }. When `base` is set it wins,
// so clear it and let `format` rebuild from `name` + `ext`.
return path.format({ dir: parsed.dir, name: parsed.name, ext })
})
const program = Effect.gen(function*() {
const result = yield* changeExtension("src/index.ts", ".js")
yield* Effect.log(result) // "src/index.js"
})

relative computes the path from one location to another, and isAbsolute tells you whether a path is already rooted. These are useful when displaying paths relative to a project root or deciding whether to resolve against a base directory:

import { Effect, Path } from "effect"
const program = Effect.gen(function*() {
const path = yield* Path.Path
const from = "/home/ada/project"
const to = "/home/ada/project/src/main.ts"
yield* Effect.log(path.relative(from, to)) // "src/main.ts"
yield* Effect.log(path.isAbsolute(to)) // true
yield* Effect.log(path.normalize("a/./b/../c")) // "a/c"
})

Converting between paths and file:// URLs is the one place Path can fail — an invalid URL or non-file: scheme yields a BadArgument. Because these return effects, the failure is typed and handled like any other:

import { Effect, Path } from "effect"
const program = Effect.gen(function*() {
const path = yield* Path.Path
// `import.meta.url` is a file:// URL; convert it to a filesystem path.
const filePath = yield* path.fromFileUrl(new URL("file:///home/ada/app.ts"))
yield* Effect.log(filePath) // "/home/ada/app.ts"
const url = yield* path.toFileUrl("/home/ada/app.ts")
yield* Effect.log(url.href) // "file:///home/ada/app.ts"
}).pipe(
Effect.catchTag("BadArgument", (error) =>
Effect.logError(`invalid path or URL: ${error.message}`)
)
)

NodePath.layer follows the host operating system, but you can force a specific style with NodePath.layerPosix (always /) or NodePath.layerWin32 (always \). This is handy when generating paths for a different target than the machine you are running on:

import { NodePath } from "@effect/platform-node"
import { Effect, Path } from "effect"
const program = Effect.gen(function*() {
const path = yield* Path.Path
yield* Effect.log(path.join("a", "b", "c"))
}).pipe(
// Always emit POSIX paths regardless of the host OS.
Effect.provide(NodePath.layerPosix)
)

Paths produced here are exactly what the FileSystem service expects, so the two services compose naturally.