# Optics

An **optic** is a first-class, reusable reference to a piece of data *inside* a
larger structure. Instead of writing manual spread-based update code every time
you need to change a deeply nested field, you build an optic once and use it to
`get`, `replace`, or `modify` that field — immutably, with structural sharing.

The whole module lives in core `effect`:

```ts
import { Optic } from "effect"
```

## Quickstart

Start a chain with `Optic.id<S>()`, drill in with `.key(...)`, then read and
update nested immutable state:

```ts
import { Optic } from "effect"

type State = { user: { name: string; age: number } }

const _age = Optic.id<State>().key("user").key("age")

const s1: State = { user: { name: "Alice", age: 30 } }

// Read
console.log(_age.get(s1))
// => 30

// Update immutably (returns a new object; s1 is untouched)
const s2 = _age.replace(31, s1)
console.log(s2)
// => { user: { name: "Alice", age: 31 } }

// Modify with a function — note modify returns a function (s: S) => S
const s3 = _age.modify((n) => n + 1)(s1)
console.log(s3)
// => { user: { name: "Alice", age: 31 } }

// Structural sharing: only nodes on the path are cloned
console.log(s2.user !== s1.user)
// => true
```

## The mental model

An optic packages up "how to look at `A` given an `S`" together with "how to put
a new `A` back into an `S`". There are five flavors, ordered by how much they
*guarantee*:

| Optic | Read | Write | Use when |
| - | - | - | - |
| **Iso** | always succeeds | always succeeds | lossless two-way conversion (`Celsius ↔ Fahrenheit`, `Record ↔ entries`) |
| **Lens** | always succeeds | needs original `S` | a part that always exists (a required struct field) |
| **Prism** | can fail | builds `S` from `A` alone | a part that may be absent (a union variant, a validated subset) |
| **Optional** | can fail | can fail | the most general case (a record key that may not exist) |
| **Traversal** | zero-or-more | zero-or-more | many elements at once (every item in an array) |

The **strength hierarchy** is `Iso > Lens | Prism > Optional`, and a `Traversal`
is technically an `Optional<S, ReadonlyArray<A>>`. `Iso` extends both `Lens` and
`Prism`; all four extend `Optional`.

When you **compose** two optics, the result is the *weaker* of the two:

```ts
import { Optic } from "effect"

// Lens . Lens = Lens
Optic.id<{ a: { b: number } }>().key("a").key("b")

// Lens . Prism = Optional (key is a Lens; .tag is a Prism)
Optic.id<{ shape: { _tag: "Circle"; r: number } | { _tag: "Sq"; s: number } }>()
  .key("shape")
  .tag("Circle")
```
**Gotchas:** - **Structural persistence, not identity-based no-op detection.** Only nodes on
  the path are cloned; unrelated branches keep referential identity. But a no-op
  update may still allocate a new root — do **not** use `===` on the root to
  detect "nothing changed".
- **`replace`/`modify` silently return the original `S` on focus failure** (e.g.
  wrong tag). They never throw. Use `replaceResult` when you need to *know* the
  write failed.
- **`.key()` / `.optionalKey()` do not work on union types** — they produce a
  compile error. Narrow the union first with `.tag()` or `.refine()`.
- **Only plain objects and arrays can be cloned.** `Object.prototype`/null-proto
  objects and arrays are fine; class instances throw at runtime on
  `replace`/`modify`.

## The optic types

### Iso

`Iso<S, A>` is a lossless, reversible conversion between `S` and `A`: `get(s)`
and `set(a)` both always succeed and round-trip (`get(set(a)) === a`). It is the
strongest optic and composes with anything.

```ts
import { Optic } from "effect"

const fahrenheit = Optic.makeIso<number, number>(
  (c) => (c * 9) / 5 + 32,
  (f) => ((f - 32) * 5) / 9
)

console.log(fahrenheit.get(100))
// => 212
console.log(fahrenheit.set(32))
// => 0
```

### Lens

`Lens<S, A>` focuses on exactly one part of `S`. `get(s)` always succeeds;
`replace(a, s)` needs the original `S` to rebuild the updated whole. It exposes
a `get` method on top of the `Optional` interface.

```ts
import { Optic } from "effect"

type Person = { readonly name: string; readonly age: number }

const _name = Optic.id<Person>().key("name")

console.log(_name.get({ name: "Alice", age: 30 }))
// => "Alice"
console.log(_name.replace("Bob", { name: "Alice", age: 30 }))
// => { name: "Bob", age: 30 }
```

### Prism

`Prism<S, A>` focuses on a part that may not be present — a union variant or a
validated subset. `getResult(s)` can fail; `set(a)` builds a new `S` from `A`
alone (no original `S` needed). See [Result](https://effect.plants.sh/data-types/result/) for the return
type.

```ts
import { Optic, Result } from "effect"

type Shape =
  | { readonly _tag: "Circle"; readonly radius: number }
  | { readonly _tag: "Rect"; readonly width: number }

const _circle = Optic.id<Shape>().tag("Circle")

console.log(Result.isSuccess(_circle.getResult({ _tag: "Circle", radius: 5 })))
// => true
console.log(Result.isFailure(_circle.getResult({ _tag: "Rect", width: 10 })))
// => true

// set builds a fresh S from A alone
console.log(_circle.set({ _tag: "Circle", radius: 9 }))
// => { _tag: "Circle", radius: 9 }
```

### Optional

`Optional<S, A>` is the most general optic: **both** reading and writing can
fail. It is the base interface every other optic extends, and the type you get
when composing across kinds (e.g. `Lens . Prism`).

- `getResult(s)` → `Result.Success<A>` or `Result.Failure<string>`.
- `replaceResult(a, s)` → `Result.Success<S>` or `Result.Failure<string>`.
- `replace(a, s)` / `modify(f)` return the original `s` on failure (never throw).

```ts
import { Optic, Result } from "effect"

type Env = { [key: string]: string }
const _home = Optic.id<Env>().at("HOME")

console.log(Result.isSuccess(_home.getResult({ HOME: "/root" })))
// => true
console.log(Result.isFailure(_home.getResult({ PATH: "/bin" })))
// => true

// replace returns the original on failure...
console.log(_home.replace("/new", { PATH: "/bin" }))
// => { PATH: "/bin" }

// ...replaceResult makes the failure explicit
console.log(Result.isFailure(_home.replaceResult("/new", { PATH: "/bin" })))
// => true
```

### Traversal

`Traversal<S, A>` focuses on **zero or more** elements of an array-like
structure. It is `Optional<S, ReadonlyArray<A>>` under the hood. Build one with
`.forEach(...)`, update every element with `.modifyAll(f)`, and extract the
matches with [`getAll`](#getall).

```ts
import { Optic, Schema } from "effect"

type S = { readonly items: ReadonlyArray<number> }

const _positive = Optic.id<S>()
  .key("items")
  .forEach((n) => n.check(Schema.isGreaterThan(0)))

console.log(_positive.modifyAll((n) => n * 2)({ items: [1, -2, 3] }))
// => { items: [2, -2, 6] }
```

## Constructors

### makeIso

`makeIso(get, set)` builds an [Iso](#iso) from two pure, lossless conversion
functions.

```ts
import { Optic } from "effect"

type Meters = { readonly value: number }
const meters = Optic.makeIso<Meters, number>(
  (m) => m.value,
  (n) => ({ value: n })
)

console.log(meters.get({ value: 100 }))
// => 100
console.log(meters.set(42))
// => { value: 42 }
```

### makeLens

`makeLens(get, replace)` builds a [Lens](#lens) from a getter and a replacer.
`replace(a, s)` should return a structurally new `S`.

```ts
import { Optic } from "effect"

const _first = Optic.makeLens<readonly [string, number], string>(
  (pair) => pair[0],
  (s, pair) => [s, pair[1]]
)

console.log(_first.get(["hello", 42]))
// => "hello"
console.log(_first.replace("world", ["hello", 42]))
// => ["world", 42]
```

### makePrism

`makePrism(getResult, set)` builds a [Prism](#prism) from a fallible getter
(return `Result.fail(message)` on mismatch) and an infallible setter.

```ts
import { Optic, Result } from "effect"

const numeric = Optic.makePrism<string, number>(
  (s) => {
    const n = Number(s)
    return Number.isNaN(n) ? Result.fail("not a number") : Result.succeed(n)
  },
  String
)

console.log(Result.isSuccess(numeric.getResult("42")))
// => true
console.log(numeric.set(42))
// => "42"
```

### makeOptional

`makeOptional(getResult, set)` builds the most general [Optional](#optional):
both the getter and the setter return a `Result`.

```ts
import { Optic, Result } from "effect"

const atKey = (key: string) =>
  Optic.makeOptional<Record<string, number>, number>(
    (s) =>
      Object.hasOwn(s, key)
        ? Result.succeed(s[key])
        : Result.fail(`Key "${key}" not found`),
    (a, s) =>
      Object.hasOwn(s, key)
        ? Result.succeed({ ...s, [key]: a })
        : Result.fail(`Key "${key}" not found`)
  )

console.log(Result.isSuccess(atKey("x").getResult({ x: 1 })))
// => true
console.log(Result.isFailure(atKey("x").getResult({ y: 2 })))
// => true
```

### fromChecks

`fromChecks(...checks)` builds a `Prism<T, T>` from one or more
[Schema checks](https://effect.plants.sh/schema/filters/). `getResult` runs every check and fails with a
combined message; `set` is the identity.

```ts
import { Optic, Result, Schema } from "effect"

const posInt = Optic.fromChecks<number>(
  Schema.isGreaterThan(0),
  Schema.isInt()
)

console.log(Result.isSuccess(posInt.getResult(3)))
// => true
console.log(Result.isFailure(posInt.getResult(-1)))
// => true
```

## Combinators

These are methods on the optic instances. The kind of optic returned follows the
strength hierarchy (a method on a `Lens` returns a `Lens`; the same method on an
`Optional` returns an `Optional`, etc.).

### compose

`a.compose(b)` chains two optics, producing the weaker of the two kinds
(`Iso + Iso = Iso`, `Lens + Prism = Optional`, ...).

```ts
import { Optic, Option } from "effect"

type State = { value: Option.Option<number> }

// Lens (.key) composed with Prism (some) => Optional<State, number>
const _inner = Optic.id<State>().key("value").compose(Optic.some())

console.log(_inner.replace(7, { value: Option.some(1) }))
// => { value: { _tag: "Some", value: 7 } }
```

### modify

`optic.modify(f)` returns a function `(s: S) => S` that applies `f` to the
focused value. On focus failure the original `s` is returned unchanged.

```ts
import { Optic } from "effect"

type S = { readonly a: { readonly b: number } }
const _b = Optic.id<S>().key("a").key("b")

console.log(_b.modify((n) => n + 1)({ a: { b: 1 } }))
// => { a: { b: 2 } }
```

### key

`.key("name")` focuses on a struct/tuple property. On a `Lens` it returns a
`Lens`; on an `Optional`, an `Optional`. Does not work on union types (compile
error).

```ts
import { Optic } from "effect"

type S = { readonly a: { readonly b: number } }
const _b = Optic.id<S>().key("a").key("b")

console.log(_b.get({ a: { b: 42 } }))
// => 42
```

### optionalKey

`.optionalKey("name")` focuses a key where setting `undefined` **removes** the
key (or splices the element out of an array/tuple). The focus type becomes
`A[Key] | undefined`. No union types.

```ts
import { Optic } from "effect"

type S = { readonly a?: number }
const _a = Optic.id<S>().optionalKey("a")

console.log(_a.replace(undefined, { a: 1 }))
// => {}
console.log(_a.replace(2, {}))
// => { a: 2 }
```

### check

`.check(...checks)` adds one or more [Schema checks](https://effect.plants.sh/schema/filters/) to the
chain. `getResult` fails when any check fails; `set` passes through unchanged. On
a `Prism` returns a `Prism`; on an `Optional`, an `Optional`.

```ts
import { Optic, Result, Schema } from "effect"

const _pos = Optic.id<number>().check(Schema.isGreaterThan(0))

console.log(Result.isSuccess(_pos.getResult(5)))
// => true
console.log(Result.isFailure(_pos.getResult(-1)))
// => true
```

### refine

`.refine(guard, annotations?)` narrows the focus to a subtype using a type
guard. Pass `annotations` to customize the failure message.

```ts
import { Optic, Result } from "effect"

type B = { readonly _tag: "b"; readonly b: number }
type S = { readonly _tag: "a"; readonly a: string } | B

const _b = Optic.id<S>().refine(
  (s: S): s is B => s._tag === "b",
  { expected: `"b" tag` }
)

console.log(Result.isSuccess(_b.getResult({ _tag: "b", b: 1 })))
// => true
```

### tag

`.tag("Variant")` narrows a tagged union to the variant with the given `_tag`. A
shorthand for `.refine(s => s._tag === tag)`.

```ts
import { Optic, Result } from "effect"

type Shape =
  | { readonly _tag: "Circle"; readonly radius: number }
  | { readonly _tag: "Rect"; readonly width: number }

const _radius = Optic.id<Shape>().tag("Circle").key("radius")

console.log(Result.isSuccess(_radius.getResult({ _tag: "Circle", radius: 5 })))
// => true
console.log(Result.isFailure(_radius.getResult({ _tag: "Rect", width: 10 })))
// => true
```

### at

`.at("key")` focuses a key only if it exists (`Object.hasOwn`). Unlike `.key()`,
both `getResult` and `replaceResult` fail when the key is absent — ideal for
records and arrays. Always returns an `Optional`.

```ts
import { Optic, Result } from "effect"

type Env = { [key: string]: number }
const _x = Optic.id<Env>().at("x")

console.log(Result.isSuccess(_x.getResult({ x: 1 })))
// => true
console.log(Result.isFailure(_x.getResult({ y: 2 })))
// => true
```

### pick

`.pick(["a", "c"])` focuses on a subset of struct keys. On a `Lens` returns a
`Lens`; on an `Optional`, an `Optional`. No union types.

```ts
import { Optic } from "effect"

type S = { readonly a: string; readonly b: number; readonly c: boolean }
const _ac = Optic.id<S>().pick(["a", "c"])

console.log(_ac.get({ a: "hi", b: 1, c: true }))
// => { a: "hi", c: true }
```

### omit

`.omit(["b"])` focuses on all keys *except* the given ones — the inverse of
`.pick()`. No union types.

```ts
import { Optic } from "effect"

type S = { readonly a: string; readonly b: number; readonly c: boolean }
const _ac = Optic.id<S>().omit(["b"])

console.log(_ac.get({ a: "hi", b: 1, c: true }))
// => { a: "hi", c: true }
```

### notUndefined

`.notUndefined()` filters `undefined` out of the focus, producing a `Prism`.
`getResult` fails when the focus is `undefined`.

```ts
import { Optic, Result } from "effect"

const _defined = Optic.id<number | undefined>().notUndefined()

console.log(Result.isSuccess(_defined.getResult(42)))
// => true
console.log(Result.isFailure(_defined.getResult(undefined)))
// => true
```

### forEach

`.forEach(f)` — **Traversal only** — focuses every element of an array-like
focus, optionally narrowing each via an element-level optic. `f` receives the
identity iso for the element type and returns a sub-optic. Returns a new
`Traversal`.

```ts
import { Optic, Schema } from "effect"

type S = { readonly items: ReadonlyArray<number> }

// only focus the positive elements
const _positive = Optic.id<S>()
  .key("items")
  .forEach((n) => n.check(Schema.isGreaterThan(0)))

console.log(Optic.getAll(_positive)({ items: [1, -2, 3] }))
// => [1, 3]
```

### modifyAll

`.modifyAll(f)` — **Traversal only** — returns `(s: S) => S` that applies `f` to
**every** focused element (vs `.modify`, which operates on the whole array). On
focus failure the original `s` is returned.

```ts
import { Optic, Schema } from "effect"

type S = { readonly items: ReadonlyArray<number> }

const _positive = Optic.id<S>()
  .key("items")
  .forEach((n) => n.check(Schema.isGreaterThan(0)))

console.log(_positive.modifyAll((n) => n * 2)({ items: [1, -2, 3] }))
// => { items: [2, -2, 6] }
```

## Built-in optics & helpers

### id

`id<S>()` is the identity [Iso](#iso) — the entry point for almost every chain.
`get(s)` returns `s` and `set(a)` returns `a`. It is a singleton.

```ts
import { Optic } from "effect"

type S = { readonly x: number }
const _x = Optic.id<S>().key("x")

console.log(_x.get({ x: 42 }))
// => 42
```

### entries

`entries<A>()` is an [Iso](#iso) converting a `Record<string, A>` to an array of
`[key, value]` pairs and back (`Object.entries` / `Object.fromEntries`). Handy
for traversing record values.

```ts
import { Optic, Schema } from "effect"

// increment only the positive record values
const _positiveValues = Optic.entries<number>()
  .forEach((entry) => entry.key(1).check(Schema.isGreaterThan(0)))

console.log(_positiveValues.modifyAll((n) => n + 1)({ a: 0, b: 3, c: -1 }))
// => { a: 0, b: 4, c: -1 }
```

### some

`some<A>()` is a [Prism](#prism) into the value inside `Option.Some`.
`getResult` fails on `None`; `set(a)` wraps `a` in `Option.some(a)`. See
[Option](https://effect.plants.sh/data-types/option/).

```ts
import { Optic, Option, Result } from "effect"

const _some = Optic.id<Option.Option<number>>().compose(Optic.some())

console.log(Result.isSuccess(_some.getResult(Option.some(42))))
// => true
console.log(Result.isFailure(_some.getResult(Option.none())))
// => true
console.log(_some.set(10))
// => { _tag: "Some", value: 10 }
```

### none

`none<A>()` is a [Prism](#prism) into `Option.None`, exposing `undefined`.
`getResult` succeeds with `undefined` on `None` and fails on `Some`;
`set(undefined)` produces `Option.none()`.

```ts
import { Optic, Option, Result } from "effect"

const _none = Optic.id<Option.Option<number>>().compose(Optic.none())

console.log(Result.isSuccess(_none.getResult(Option.none())))
// => true
console.log(Result.isFailure(_none.getResult(Option.some(1))))
// => true
```

### success

`success<A, E>()` is a [Prism](#prism) into the success value of a
[Result](https://effect.plants.sh/data-types/result/). `getResult` fails on `Failure`; `set(a)` produces
`Result.succeed(a)`.

```ts
import { Optic, Result } from "effect"

const _ok = Optic.id<Result.Result<number, string>>().compose(Optic.success())

console.log(Result.isSuccess(_ok.getResult(Result.succeed(42))))
// => true
console.log(Result.isFailure(_ok.getResult(Result.fail("err"))))
// => true
```

### failure

`failure<A, E>()` is a [Prism](#prism) into the failure value of a
[Result](https://effect.plants.sh/data-types/result/). `getResult` fails on `Success`; `set(e)` produces
`Result.fail(e)`.

```ts
import { Optic, Result } from "effect"

const _err = Optic.id<Result.Result<number, string>>().compose(Optic.failure())

console.log(Result.isSuccess(_err.getResult(Result.fail("oops"))))
// => true
console.log(Result.isFailure(_err.getResult(Result.succeed(42))))
// => true
```

### getAll

`getAll(traversal)` returns a function `(s: S) => Array<A>` that extracts all
focused elements as a fresh, mutable array (empty when the traversal cannot
focus).

```ts
import { Optic, Schema } from "effect"

type S = { readonly values: ReadonlyArray<number> }

const _pos = Optic.id<S>()
  .key("values")
  .forEach((n) => n.check(Schema.isGreaterThan(0)))

const getPositive = Optic.getAll(_pos)

console.log(getPositive({ values: [3, -1, 5] }))
// => [3, 5]
console.log(getPositive({ values: [-1, -2] }))
// => []
```

## Real-world example

Optics shine when updating immutable application state. Here a single traversal
both *reads* and *bumps* the like counts of every already-liked post, deep inside
nested state — no manual spreads, no mutation.

```ts
import { Optic, Schema } from "effect"

type Post = { title: string; likes: number }
type AppState = { user: { posts: ReadonlyArray<Post> } }

// A reusable Traversal focused on the likes of every post that has > 0 likes
const _likedPostLikes = Optic.id<AppState>()
  .key("user")
  .key("posts")
  .forEach((post) => post.key("likes").check(Schema.isGreaterThan(0)))

const state: AppState = {
  user: { posts: [{ title: "a", likes: 0 }, { title: "b", likes: 1 }] }
}

// Read: which posts are already liked, and by how much?
console.log(Optic.getAll(_likedPostLikes)(state))
// => [1]

// Update: add one like to every already-liked post (the unliked one is skipped)
const addLike = _likedPostLikes.modifyAll((n) => n + 1)

console.log(addLike(state))
// => { user: { posts: [{ title: "a", likes: 0 }, { title: "b", likes: 2 }] } }

// The original is untouched
console.log(state.user.posts[1].likes)
// => 1
```

Because optics are plain values, you can store `_likedPostLikes` on a service,
pass it around, and reuse the exact same accessor for reads, validated writes,
and bulk updates throughout your application.

## See also

- [Option](https://effect.plants.sh/data-types/option/) — for the `some` / `none` prisms.
- [Result](https://effect.plants.sh/data-types/result/) — the return type of `getResult` / `replaceResult`, and the target of the `success` / `failure` prisms.
- [Schema filters](https://effect.plants.sh/schema/filters/) — the checks used by `.check()` and `fromChecks()`.