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))Building paths: join vs resolve
Section titled “Building paths: join vs resolve”Both combine segments, but they answer different questions.
joinis pure concatenation: it glues segments with the separator, collapses redundant slashes, and normalizes./..— but it never reaches outside the string you gave it. The result is absolute only if the first segment already is.resolvecomputes an absolute path. It processes segments right-to-left until it finds an absolute one; if none is absolute, it anchors the result to the current working directory (process.cwd()on Node).
import { Effect, Path } from "effect"
const program = Effect.gen(function*() { const path = yield* Path.Path
// join: relative in, relative out. yield* Effect.log(path.join("a", "b", "..", "c")) // "a/c" yield* Effect.log(path.join("/var", "log", "app")) // "/var/log/app"
// resolve: always absolute. Relative to cwd when no segment is rooted. yield* Effect.log(path.resolve("a", "b")) // e.g. "/current/working/dir/a/b"
// The right-most absolute segment wins; earlier segments are discarded. yield* Effect.log(path.resolve("/etc", "/usr", "bin")) // "/usr/bin"})Use join to assemble a path under a known base. Use resolve when you need a
canonical absolute path (e.g. to compare two locations or pass to the file
system regardless of where the process started).
Inspecting and decomposing paths
Section titled “Inspecting and decomposing paths”These pure helpers pull a path apart. basename optionally strips a known
suffix, and parse / format round-trip an entire path through a structured
object.
import { Effect, Path } from "effect"
const program = Effect.gen(function*() { const path = yield* Path.Path const file = "/home/ada/project/main.ts"
yield* Effect.log(path.basename(file)) // "main.ts" yield* Effect.log(path.basename(file, ".ts")) // "main" (suffix stripped) yield* Effect.log(path.dirname(file)) // "/home/ada/project" yield* Effect.log(path.extname(file)) // ".ts" yield* Effect.log(path.isAbsolute(file)) // true yield* Effect.log(path.sep) // "/" (POSIX) or "\\" (win32)
// parse -> Path.Parsed, format rebuilds the string. They are inverses. const parsed = path.parse(file) // { root: "/", dir: "/home/ada/project", base: "main.ts", ext: ".ts", name: "main" } yield* Effect.log(path.format(parsed)) // "/home/ada/project/main.ts"})Parsing and formatting
Section titled “Parsing and formatting”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 and normalized paths
Section titled “Relative and normalized paths”relative computes the path from one location to another, isAbsolute tells
you whether a path is already rooted, and normalize collapses ./..
segments and redundant separators. These are useful when displaying paths
relative to a project root or cleaning up user-supplied input:
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.relative(to, from)) // "../.." yield* Effect.log(path.isAbsolute(to)) // true yield* Effect.log(path.normalize("a/./b/../c")) // "a/c"
// toNamespacedPath is a no-op on POSIX; on Windows it adds the \\?\ prefix. yield* Effect.log(path.toNamespacedPath("/home/ada")) // "/home/ada" (POSIX)})File URLs
Section titled “File URLs”Converting between paths and file:// URLs is the one place Path can fail —
an invalid URL or non-file: scheme yields a BadArgument (a
PlatformError). 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}`) ))fromFileUrl rejects URLs whose scheme is not file:, that carry a hostname,
or that contain percent-encoded path separators (%2F). Every other method on
the service is a pure, synchronous string operation that cannot fail.
Choosing a separator style
Section titled “Choosing a separator style”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))The core effect package also ships a built-in POSIX implementation as
Path.layer, so code that does not depend on @effect/platform-node can still
satisfy the Path requirement (with / separators and POSIX semantics):
import { Effect, Path } from "effect"
const program = Effect.gen(function*() { const path = yield* Path.Path yield* Effect.log(path.join("a", "b", "c")) // "a/b/c"}).pipe(Effect.provide(Path.layer))Paths produced here are exactly what the FileSystem
service expects, so the two services compose naturally.
API reference
Section titled “API reference”The Path service is accessed through the Path.Path tag. Every method below
is a member of the Path interface. All are pure synchronous string operations
except fromFileUrl and toFileUrl, which return an Effect.
Path.Path
Section titled “Path.Path”The Context.Service tag used to access the current path implementation from
the environment. Provide it with Path.layer, NodePath.layer, or a custom
implementation.
import { Effect, Path } from "effect"
const program = Effect.gen(function*() { const path = yield* Path.Path // resolve the service return path.join("a", "b")}) // => Effect<string, never, Path.Path>Path.layer
Section titled “Path.layer”A Layer providing the built-in POSIX implementation from the core effect
package. Separator is /; semantics are POSIX.
import { Effect, Path } from "effect"
const run = Effect.gen(function*() { const path = yield* Path.Path return path.sep // => "/"}).pipe(Effect.provide(Path.layer))The platform path-segment separator as a string.
path.sep // => "/" on POSIX, "\\" on Windowsbasename
Section titled “basename”Returns the last portion of a path. An optional second argument is a suffix to strip from the result.
path.basename("/foo/bar/baz.html") // => "baz.html"path.basename("/foo/bar/baz.html", ".html") // => "baz"path.basename("/foo/bar/") // => "bar"dirname
Section titled “dirname”Returns the directory portion of a path (everything before the last segment).
path.dirname("/foo/bar/baz.html") // => "/foo/bar"path.dirname("file.txt") // => "."path.dirname("/foo") // => "/"extname
Section titled “extname”Returns the extension of the path — the substring from the last . to the end
of the last segment — or an empty string when there is none.
path.extname("index.html") // => ".html"path.extname("index.") // => "."path.extname("index") // => ""path.extname(".gitignore") // => "" (leading dot is not an extension)format
Section titled “format”Builds a path string from a partial Path.Parsed object. base takes
precedence over name + ext, and dir takes precedence over root.
path.format({ dir: "/home/ada", name: "file", ext: ".ts" }) // => "/home/ada/file.ts"path.format({ root: "/", base: "file.txt" }) // => "/file.txt"path.format({ name: "file", ext: ".txt" }) // => "file.txt"fromFileUrl
Section titled “fromFileUrl”Converts a file: URL into a filesystem path. Fails with BadArgument when
the scheme is not file:, a hostname is present, or the path contains an
encoded separator (%2F). Returns Effect<string, BadArgument>.
import { Effect, Path } from "effect"
Effect.gen(function*() { const path = yield* Path.Path return yield* path.fromFileUrl(new URL("file:///home/ada/app.ts")) // => "/home/ada/app.ts"})isAbsolute
Section titled “isAbsolute”Returns whether the path is absolute (rooted).
path.isAbsolute("/foo/bar") // => truepath.isAbsolute("foo/bar") // => falsepath.isAbsolute("") // => falseJoins all segments with the platform separator, then normalizes the result.
Empty segments are ignored; with no segments it returns ".".
path.join("/foo", "bar", "baz/asdf", "quux", "..") // => "/foo/bar/baz/asdf"path.join("a", "", "b") // => "a/b"path.join() // => "."normalize
Section titled “normalize”Resolves . and .. segments and collapses redundant separators. Does not
make the path absolute and preserves a trailing separator if present.
path.normalize("/foo/bar//baz/asdf/quux/..") // => "/foo/bar/baz/asdf"path.normalize("a/./b/../c") // => "a/c"path.normalize("") // => "."Decomposes a path into a Path.Parsed object: { root, dir, base, ext, name }.
The inverse of format.
path.parse("/home/ada/file.txt")// => { root: "/", dir: "/home/ada", base: "file.txt", ext: ".txt", name: "file" }
path.parse("file.txt")// => { root: "", dir: "", base: "file.txt", ext: ".txt", name: "file" }relative
Section titled “relative”Returns the relative path from from to to. Both arguments are resolved to
absolute paths first; equal paths yield an empty string.
path.relative("/data/orandea/test/aaa", "/data/orandea/impl/bbb")// => "../../impl/bbb"path.relative("/home/ada/project", "/home/ada/project/src/main.ts")// => "src/main.ts"resolve
Section titled “resolve”Resolves segments right-to-left into an absolute path, anchoring to the current
working directory if no absolute segment is found. Processes . and ...
path.resolve("/foo/bar", "./baz") // => "/foo/bar/baz"path.resolve("/foo/bar", "/tmp/file") // => "/tmp/file"path.resolve("wwwroot", "static_files/png/", "../gif/image.gif")// => "<cwd>/wwwroot/static_files/gif/image.gif"toFileUrl
Section titled “toFileUrl”Converts a filesystem path into a file: URL. The path is resolved to
absolute first and special characters are percent-encoded. Returns
Effect<URL, BadArgument>.
import { Effect, Path } from "effect"
Effect.gen(function*() { const path = yield* Path.Path const url = yield* path.toFileUrl("/home/ada/app.ts") return url.href // => "file:///home/ada/app.ts"})toNamespacedPath
Section titled “toNamespacedPath”Returns the namespace-prefixed path. A no-op on POSIX; on Windows it prefixes
absolute paths with \\?\ (the extended-length namespace).
// POSIX implementation:path.toNamespacedPath("/home/ada") // => "/home/ada"// Windows implementation:// path.toNamespacedPath("C:\\foo") // => "\\\\?\\C:\\foo"Path.Parsed
Section titled “Path.Parsed”The structured object produced by parse and consumed by format. All fields
are strings.
interface Parsed { readonly root: string // filesystem root, e.g. "/" (or "" if relative) readonly dir: string // full directory, e.g. "/home/ada" readonly base: string // file name with extension, e.g. "file.txt" readonly ext: string // extension including the dot, e.g. ".txt" readonly name: string // file name without extension, e.g. "file"}For a worked example, parse a path and rebuild it after changing one field — see Parsing and formatting above.
Providing a custom implementation
Section titled “Providing a custom implementation”Path.Path is an ordinary service tag, so you can supply your own
implementation (for tests, virtual file systems, or alternate platforms) with
Layer.succeed. The implementation must include the Path.TypeId marker:
import { Effect, Layer, Path } from "effect"
const fakePath = Path.Path.of({ [Path.TypeId]: Path.TypeId, sep: "/", basename: (p) => p.split("/").pop() ?? "", dirname: (p) => p.split("/").slice(0, -1).join("/") || "/", extname: (p) => p.match(/\.[^.]*$/)?.[0] ?? "", format: ({ dir, name, ext }) => `${dir ?? ""}/${name ?? ""}${ext ?? ""}`, fromFileUrl: (url) => Effect.succeed(url.pathname), isAbsolute: (p) => p.startsWith("/"), join: (...ps) => ps.join("/"), normalize: (p) => p.replace(/\/+/g, "/"), parse: (p) => ({ root: "", dir: "", base: p, ext: "", name: p }), relative: (from, to) => to.replace(from, ""), resolve: (...ps) => ps.join("/"), toFileUrl: (p) => Effect.succeed(new URL(`file://${p}`)), toNamespacedPath: (p) => p})
const testLayer = Layer.succeed(Path.Path)(fakePath)