Skip to content

FileSystem

The FileSystem service is Effect’s abstraction over the file system. Every method returns an Effect that fails with a PlatformError instead of throwing, so missing files, permission errors, and bad arguments all flow through the typed error channel. You depend on the abstract FileSystem interface and provide a concrete implementation — NodeFileSystem.layer on Node.js — at the edge of your program.

import { NodeFileSystem } from "@effect/platform-node"
import { Context, Effect, FileSystem, Layer, PlatformError } from "effect"
// A small service that persists JSON snapshots to a directory.
class SnapshotStore extends Context.Service<SnapshotStore, {
readonly save: (name: string, value: unknown) => Effect.Effect<void, PlatformError.PlatformError>
readonly load: (name: string) => Effect.Effect<unknown, PlatformError.PlatformError>
}>()("app/SnapshotStore") {
static readonly layer = Layer.effect(
SnapshotStore,
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const dir = "snapshots"
// Ensure the target directory exists. `recursive` makes this a no-op
// when the directory is already present, like `mkdir -p`.
yield* fs.makeDirectory(dir, { recursive: true })
const save = (name: string, value: unknown) =>
// `writeFileString` writes (and creates) the file in one call.
fs.writeFileString(`${dir}/${name}.json`, JSON.stringify(value, null, 2))
const load = (name: string) =>
fs.readFileString(`${dir}/${name}.json`).pipe(
Effect.map((text) => JSON.parse(text) as unknown)
)
return { save, load } as const
})
).pipe(
// The store needs a concrete FileSystem; wire in the Node implementation.
Layer.provide(NodeFileSystem.layer)
)
}
const program = Effect.gen(function*() {
const store = yield* SnapshotStore
yield* store.save("user-42", { id: 42, name: "Ada" })
const restored = yield* store.load("user-42")
yield* Effect.log(restored)
}).pipe(Effect.provide(SnapshotStore.layer))

Request the service with yield* FileSystem.FileSystem inside an effect, then provide a platform layer when you run the program. On Node.js that layer is NodeFileSystem.layer from @effect/platform-node:

import { NodeFileSystem } from "@effect/platform-node"
import { Effect, FileSystem } from "effect"
const program = Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
return yield* fs.readFileString("package.json")
})
// `FileSystem.FileSystem` is the requirement; the Node layer satisfies it.
const runnable = program.pipe(Effect.provide(NodeFileSystem.layer))

For tests you do not need a real file system: FileSystem.layerNoop provides a stub whose behavior you override per test (see the reference at the end of this page).

The most common operations come in byte and string flavours. Use the String variants when you are working with text and the raw variants when you need exact bytes. Both writers accept flag (an OpenFlag) and mode options:

import { Effect, FileSystem } from "effect"
const program = Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
// Text helpers return / accept `string`.
yield* fs.writeFileString("notes.txt", "first line\n")
const text = yield* fs.readFileString("notes.txt")
// => "first line\n"
// `readFileString` takes an optional encoding (passed to TextDecoder).
const latin1 = yield* fs.readFileString("legacy.txt", "latin1")
// Byte helpers return / accept `Uint8Array`.
const bytes: Uint8Array = yield* fs.readFile("notes.txt")
yield* fs.writeFile("copy.bin", bytes)
// `flag: "a"` appends instead of truncating; `mode` sets POSIX permissions.
yield* fs.writeFileString("notes.txt", "second line\n", { flag: "a", mode: 0o644 })
yield* Effect.log(`${text.length} chars, ${bytes.length} bytes`)
})

exists, stat, and readDirectory let you query the file system before acting on it. Note that timestamps on stat are wrapped in Option because not every platform reports them:

import { Effect, FileSystem, Option } from "effect"
const describe = Effect.fn("describe")(function*(path: string) {
const fs = yield* FileSystem.FileSystem
if (!(yield* fs.exists(path))) {
return `${path} does not exist`
}
// `stat` returns a `File.Info`: type, size (a branded bigint), mode, and
// optional timestamps.
const info = yield* fs.stat(path)
const modified = Option.match(info.mtime, {
onNone: () => "unknown",
onSome: (date) => date.toISOString()
})
return `${info.type} — ${info.size} bytes — modified ${modified}`
})

To enumerate a directory, readDirectory returns the entry names; pass recursive: true to walk nested directories:

import { Effect, FileSystem } from "effect"
const listTree = Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
return yield* fs.readDirectory("src", { recursive: true })
})

makeDirectory, remove, copy, copyFile, and rename cover the usual directory and path-management tasks. recursive and force options mirror the familiar shell flags:

import { Effect, FileSystem } from "effect"
const program = Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
// `recursive: true` is like `mkdir -p` — creates parents, no error if present.
yield* fs.makeDirectory("build/artifacts", { recursive: true })
// `copy` is recursive like `cp -r`; `copyFile` copies a single file.
yield* fs.copy("src", "build/src")
yield* fs.copyFile("README.md", "build/README.md")
// `rename` moves or renames a file or directory.
yield* fs.rename("build/README.md", "build/README.txt")
// `recursive` removes nested entries; `force` ignores a missing path.
yield* fs.remove("build", { recursive: true, force: true })
})

Reading a multi-gigabyte file into memory with readFile is wasteful. The stream method returns a Stream of byte chunks so you can process a file incrementally, and sink gives you a writable counterpart. Here we count the lines of a file without ever holding it all in memory:

import { Effect, FileSystem, Stream } from "effect"
const countLines = Effect.fn("countLines")(function*(path: string) {
const fs = yield* FileSystem.FileSystem
return yield* fs.stream(path).pipe(
Stream.decodeText(), // Uint8Array chunks -> string chunks
Stream.splitLines, // re-chunk on line boundaries
Stream.runFold(() => 0, (count) => count + 1)
)
})

You can tune chunkSize, offset, and bytesToRead on stream to read a specific window of a file. Use the Size helpers (KiB, MiB, …) for readable byte counts:

import { Effect, FileSystem, Stream } from "effect"
const tail64KiB = Effect.fn("tail64KiB")(function*(path: string) {
const fs = yield* FileSystem.FileSystem
const { size } = yield* fs.stat(path)
// Read the last 64 KiB in 8 KiB chunks.
return fs.stream(path, {
offset: size - FileSystem.KiB(64),
bytesToRead: FileSystem.KiB(64),
chunkSize: FileSystem.KiB(8)
})
})

sink is the inverse: pipe a Stream<Uint8Array> into a file. Combine it with Stream.run to copy or transform data with backpressure:

import { Effect, FileSystem, Stream } from "effect"
const copyStreamed = Effect.fn("copyStreamed")(function*(from: string, to: string) {
const fs = yield* FileSystem.FileSystem
// Read `from` as a Stream and drain it into `to` via the writable Sink.
return yield* Stream.run(fs.stream(from), fs.sink(to))
})

Temporary files and directories are tied to a Scope through the *Scoped variants, so they are deleted automatically when the scope closes — even if the effect fails:

import { Effect, FileSystem } from "effect"
const withScratch = Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
// Created inside the surrounding scope; removed when the scope closes.
const dir = yield* fs.makeTempDirectoryScoped({ prefix: "build-" })
yield* fs.writeFileString(`${dir}/manifest.json`, "{}")
yield* Effect.log(`working in ${dir}`)
}).pipe(
// `Effect.scoped` provides and closes the scope, triggering cleanup.
Effect.scoped
)

The non-scoped makeTempFile / makeTempDirectory return the path and leave cleanup to you — reach for them only when the temp resource must outlive the current scope.

Opening a file with fs.open is similarly scoped: the returned File handle is closed when the scope ends, so you never leak file descriptors.

watch returns a Stream of WatchEvent values (Create, Update, Remove) so you can react to file system activity — handy for dev tooling and hot reload:

import { Effect, FileSystem, Stream } from "effect"
const watchSrc = Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
yield* fs.watch("src").pipe(
Stream.runForEach((event) =>
// event._tag is "Create" | "Update" | "Remove"; event.path is the path.
Effect.log(`${event._tag}: ${event.path}`)
)
)
})

When whole-file and streaming helpers are not enough, open gives you a File handle for positioned reads and writes. The handle is scoped, so it is closed automatically:

import { Effect, FileSystem } from "effect"
const patch = Effect.fn("patch")(function*(path: string) {
const fs = yield* FileSystem.FileSystem
// The handle is closed when the surrounding scope ends.
const file = yield* fs.open(path, { flag: "r+" })
// Move the cursor 10 bytes from the start, then overwrite 5 bytes.
yield* file.seek(10, "start")
yield* file.writeAll(new TextEncoder().encode("HELLO"))
yield* file.sync // flush to disk
// `readAlloc` allocates a buffer and returns Option.none() at EOF.
yield* file.seek(0, "start")
return yield* file.readAlloc(FileSystem.KiB(4))
}, Effect.scoped)

Because failures are values, narrow them with Effect.catchTag. A PlatformError exposes a human-readable message and a structured reason (either a SystemError or a BadArgument):

import { Effect, FileSystem } from "effect"
const readOrDefault = Effect.fn("readOrDefault")(function*(path: string) {
const fs = yield* FileSystem.FileSystem
return yield* fs.readFileString(path)
}).pipe(
// Fall back to an empty document when the read fails for any reason.
Effect.catchTag("PlatformError", (error) =>
Effect.as(Effect.logWarning(`read failed: ${error.message}`), "")
)
)

See Manipulate paths with Path for building the paths you pass to these methods in a cross-platform way.

Every method below is accessed off the service value (const fs = yield* FileSystem.FileSystem). All return effects fail with PlatformError.

Checks whether a path can be accessed, optionally at a given access level (readable / writable). Fails if the check does not pass.

import { Effect, FileSystem } from "effect"
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
yield* fs.access("config.json", { readable: true })
// => void (succeeds) | fails with PlatformError if not readable
})

Changes the permission bits of a file to the given numeric mode.

import { Effect, FileSystem } from "effect"
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
yield* fs.chmod("deploy.sh", 0o755) // rwxr-xr-x
// => void
})

Changes the owner (uid) and group (gid) of a file.

import { Effect, FileSystem } from "effect"
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
yield* fs.chown("data.db", 1000, 1000)
// => void
})

Recursively copies a file or directory from fromPath to toPath (like cp -r). Options: overwrite, preserveTimestamps.

import { Effect, FileSystem } from "effect"
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
yield* fs.copy("src", "backup/src", { overwrite: true })
// => void
})

Copies a single file from fromPath to toPath.

import { Effect, FileSystem } from "effect"
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
yield* fs.copyFile("a.txt", "b.txt")
// => void
})

Returns true if a path exists, false otherwise. Unlike access, a missing path is not an error.

import { Effect, FileSystem } from "effect"
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const has = yield* fs.exists("config.json")
// => true | false
})

Creates a hard link at toPath pointing to fromPath.

import { Effect, FileSystem } from "effect"
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
yield* fs.link("original.txt", "hardlink.txt")
// => void
})

Creates a directory. Options: recursive (create missing parents, like mkdir -p) and mode.

import { Effect, FileSystem } from "effect"
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
yield* fs.makeDirectory("a/b/c", { recursive: true })
// => void
})

Creates a temporary directory and returns its path. Options: directory (parent location) and prefix. You are responsible for removing it.

import { Effect, FileSystem } from "effect"
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const dir = yield* fs.makeTempDirectory({ prefix: "job-" })
// => "/tmp/job-Xy3kP1" (path string)
})

Like makeTempDirectory, but the directory is deleted when the surrounding Scope closes. Requires Scope in the environment.

import { Effect, FileSystem } from "effect"
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const dir = yield* fs.makeTempDirectoryScoped({ prefix: "job-" })
// => path string; removed automatically on scope close
}).pipe(Effect.scoped)

Creates a temporary file with a randomly generated name and returns its path. Options: directory, prefix, suffix.

import { Effect, FileSystem } from "effect"
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const file = yield* fs.makeTempFile({ suffix: ".json" })
// => "/tmp/Ab12Cd.json" (path string)
})

Like makeTempFile, but the file is deleted when the Scope closes.

import { Effect, FileSystem } from "effect"
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const file = yield* fs.makeTempFileScoped({ prefix: "upload-" })
// => path string; removed automatically on scope close
}).pipe(Effect.scoped)

Opens a file and returns a scoped File handle (closed on scope exit). Options: flag (an OpenFlag, default "r") and mode.

import { Effect, FileSystem } from "effect"
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const file = yield* fs.open("log.txt", { flag: "a" })
// => File handle (auto-closed when scope ends)
}).pipe(Effect.scoped)

Lists the entry names of a directory. Option: recursive to walk nested directories.

import { Effect, FileSystem } from "effect"
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const entries = yield* fs.readDirectory("src")
// => ["index.ts", "util.ts", ...]
})

Reads an entire file into a Uint8Array.

import { Effect, FileSystem } from "effect"
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const bytes = yield* fs.readFile("image.png")
// => Uint8Array(...)
})

Reads an entire file and decodes it to a string. Optional encoding is passed to TextDecoder (default UTF-8).

import { Effect, FileSystem } from "effect"
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const text = yield* fs.readFileString("notes.txt")
// => "file contents"
})

Reads the target of a symbolic link.

import { Effect, FileSystem } from "effect"
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const target = yield* fs.readLink("current")
// => "releases/v2" (the link's destination)
})

Resolves a path to its canonical absolute pathname, following symlinks.

import { Effect, FileSystem } from "effect"
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const abs = yield* fs.realPath("./src/../package.json")
// => "/home/me/project/package.json"
})

Removes a file or directory. Options: recursive (remove nested entries) and force (ignore a missing path).

import { Effect, FileSystem } from "effect"
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
yield* fs.remove("tmp", { recursive: true, force: true })
// => void
})

Moves or renames a file or directory from oldPath to newPath.

import { Effect, FileSystem } from "effect"
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
yield* fs.rename("draft.md", "final.md")
// => void
})

Creates a writable Sink that writes incoming Uint8Array chunks to a file. Options: flag (default "w") and mode.

import { Effect, FileSystem, Stream } from "effect"
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const data = Stream.make(new TextEncoder().encode("hello\n"))
yield* Stream.run(data, fs.sink("out.txt"))
// => void (file "out.txt" now contains "hello\n")
})

Returns metadata for a path as a File.Info.

import { Effect, FileSystem } from "effect"
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const info = yield* fs.stat("package.json")
// => { type: "File", size: 1234n, mode: 420, mtime: Option.some(...), ... }
})

Creates a readable Stream of Uint8Array chunks for a file. Options: offset (start position), bytesToRead (limit), and chunkSize (default 64 KiB) — all SizeInput values.

import { Effect, FileSystem, Stream } from "effect"
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const bytes = yield* fs.stream("big.log", { chunkSize: FileSystem.KiB(16) }).pipe(
Stream.runFold(() => 0, (acc, chunk) => acc + chunk.length)
)
// => total number of bytes read
})

Creates a symbolic link at toPath pointing to fromPath.

import { Effect, FileSystem } from "effect"
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
yield* fs.symlink("releases/v2", "current")
// => void
})

Truncates a file to a given length (a SizeInput), defaulting to 0.

import { Effect, FileSystem } from "effect"
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
yield* fs.truncate("data.bin", FileSystem.KiB(4))
// => void (file is now exactly 4 KiB)
})

Sets the access (atime) and modification (mtime) timestamps of a file. Accepts Date or epoch-millisecond numbers.

import { Effect, FileSystem } from "effect"
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const now = new Date()
yield* fs.utimes("notes.txt", now, now)
// => void
})

Creates a Stream of WatchEvent values for a file or directory. Watch behavior is platform-dependent.

import { Effect, FileSystem, Stream } from "effect"
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
yield* fs.watch("src").pipe(
Stream.runForEach((e) => Effect.log(`${e._tag} ${e.path}`))
)
// => emits { _tag: "Update", path: "src/index.ts" }, ...
})

Writes a Uint8Array to a file. Options: flag (an OpenFlag) and mode.

import { Effect, FileSystem } from "effect"
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
yield* fs.writeFile("out.bin", new Uint8Array([1, 2, 3]))
// => void
})

Encodes a string (UTF-8) and writes it to a file. Same flag / mode options as writeFile; use flag: "a" to append.

import { Effect, FileSystem } from "effect"
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
yield* fs.writeFileString("log.txt", "line\n", { flag: "a" })
// => void
})

open returns a File handle for low-level, positioned I/O. It carries a branded fd (File.Descriptor) and the members below.

An effect that returns the handle’s File.Info.

const info = yield* file.stat
// => { type: "File", size: 42n, ... }

Moves the read/write cursor by offset (SizeInput) relative to a SeekMode ("start" or "current"). Cannot fail.

yield* file.seek(128, "start") // absolute position 128
yield* file.seek(16, "current") // 16 bytes forward
// => void

Reads bytes into the provided buffer and returns the number of bytes read as a Size (bigint).

const buffer = new Uint8Array(64)
const bytesRead = yield* file.read(buffer)
// => 64n (or fewer near EOF)

Allocates a buffer of the given size (SizeInput), reads into it, and returns Option.some(bytes) — or Option.none() at end of file.

import { Option } from "effect"
const chunk = yield* file.readAlloc(FileSystem.KiB(4))
Option.match(chunk, {
onNone: () => "EOF",
onSome: (bytes) => `read ${bytes.length} bytes`
})
// => "read 4096 bytes" | "EOF"

Writes buffer at the current cursor and returns the number of bytes written as a Size.

const written = yield* file.write(new TextEncoder().encode("hi"))
// => 2n

Writes the entire buffer, looping internally until all bytes are flushed.

yield* file.writeAll(new TextEncoder().encode("complete payload"))
// => void

Truncates the open file to length (SizeInput), defaulting to 0.

yield* file.truncate(FileSystem.KiB(1))
// => void

An effect that flushes buffered data and metadata to the storage device.

yield* file.sync
// => void

The handle is closed automatically when its Scope ends — there is no explicit close method; rely on Effect.scoped (or open inside a Layer.effect).

The metadata record returned by stat and file.stat. Timestamps and several numeric fields are wrapped in Option because not all platforms report them.

FieldTypeNotes
typeFile.Type"File", "Directory", "SymbolicLink", …
sizeSizebyte count (branded bigint)
modenumberPOSIX permission bits
mtime / atime / birthtimeOption<Date>modify / access / creation time
devnumberdevice id
inoOption<number>inode number
nlinkOption<number>hard link count
uid / gidOption<number>owner / group ids
rdevOption<number>device id for special files
blksizeOption<Size>block size for I/O
blocksOption<number>number of allocated blocks
import { Effect, FileSystem, Option } from "effect"
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const info: FileSystem.File.Info = yield* fs.stat("notes.txt")
const created = Option.getOrElse(info.birthtime, () => new Date(0))
// => info.type === "File", info.size is a bigint
})

The kind of a file system entry: "File", "Directory", "SymbolicLink", "BlockDevice", "CharacterDevice", "FIFO", "Socket", or "Unknown".

const info = yield* fs.stat("/dev/null")
if (info.type === "CharacterDevice") { /* ... */ }

A branded number identifying an open file, exposed as file.fd. Construct one with FileSystem.FileDescriptor when implementing a custom FileSystem.

import { FileSystem } from "effect"
const fd = FileSystem.FileDescriptor(3)
// => branded 3 (File.Descriptor)

POSIX-style open mode accepted by open, writeFile, writeFileString, and sink. The full set: "r", "r+", "w", "wx", "w+", "wx+", "a", "ax", "a+", "ax+". Read more in the inline list below.

  • "r" — read; file must exist.
  • "r+" — read/write; file must exist.
  • "w" — write; truncates or creates.
  • "wx" — like "w" but fails if the file exists.
  • "w+" — read/write; truncates or creates.
  • "wx+" — like "w+" but fails if the file exists.
  • "a" — append; creates if missing.
  • "ax" — like "a" but fails if the file exists.
  • "a+" — read/append; creates if missing.
  • "ax+" — like "a+" but fails if the file exists.
yield* fs.open("new.txt", { flag: "wx" }) // create exclusively

Reference point for file.seek: "start" (from the beginning of the file) or "current" (from the current cursor position).

yield* file.seek(0, "start") // rewind to beginning
yield* file.seek(16, "current") // advance 16 bytes from the cursor

Size is a branded bigint byte count. SizeInput is bigint | number | Size — anywhere a size is accepted you may pass any of them. Use the constructors for readable values; all return a Size.

import { FileSystem } from "effect"
FileSystem.Size(1024) // => 1024n
FileSystem.KiB(64) // => 65536n (64 * 1024)
FileSystem.MiB(10) // => 10485760n
FileSystem.GiB(1) // => 1073741824n
FileSystem.TiB(1) // => 1099511627776n
FileSystem.PiB(2) // => 2251799813685248n

The events emitted by watch. A tagged union of { _tag: "Create", path }, { _tag: "Update", path }, and { _tag: "Remove", path }.

import { Effect, FileSystem, Stream } from "effect"
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
yield* fs.watch(".").pipe(
Stream.runForEach((event) => {
switch (event._tag) {
case "Create": return Effect.log(`created ${event.path}`)
case "Update": return Effect.log(`changed ${event.path}`)
case "Remove": return Effect.log(`deleted ${event.path}`)
}
})
)
})

Builds a concrete FileSystem from a partial implementation, deriving exists, readFileString, stream, sink, and writeFileString from the core methods you provide. Used by platform packages to define their layer.

import { FileSystem } from "effect"
declare const coreImpl: Omit<
FileSystem.FileSystem,
"~effect/platform/FileSystem" | "exists" | "readFileString" | "stream" | "sink" | "writeFileString"
>
const fs = FileSystem.make(coreImpl)
// => a full FileSystem

FileSystem.makeNoop / FileSystem.layerNoop

Section titled “FileSystem.makeNoop / FileSystem.layerNoop”

makeNoop returns a stub FileSystem for tests: by default exists returns false, remove succeeds, most reads fail with NotFound, and temp operations die as not implemented. Pass overrides for the methods a test needs. layerNoop wraps the same overrides in a Layer.

import { Effect, FileSystem } from "effect"
const testLayer = FileSystem.layerNoop({
readFileString: () => Effect.succeed("mocked content"),
exists: () => Effect.succeed(true)
})
const program = Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
return yield* fs.readFileString("anything.txt")
// => "mocked content"
}).pipe(Effect.provide(testLayer))

Type guard that narrows an unknown value to a File by checking its runtime marker.

import { FileSystem } from "effect"
declare const value: unknown
if (FileSystem.isFile(value)) {
// value is FileSystem.File here
}