@fuiste/optics

中文文档
2.0.0发布于 2026/04/11MITTypeScriptES Module零依赖近 30 天下载 7274

Type-safe, functional optics (lenses, prisms, traversals, and more) for immutable data

原始使用文档
文档目录23 节
  1. Installation
  2. Development
  3. Quick start
  4. Lens (required data)
  5. Prism (optional data)
  6. Iso (invertible mapping)
  7. Traversal (multiple values)
  8. Getter (computed read-only value)
  9. Fold (read-only extraction of many values)
  10. Composition
  11. Multi-step chains
  12. Combinators
  13. guard — type-guard prism
  14. at — record key access
  15. index — array element access
  16. each — array traversal
  17. API reference
  18. Types
  19. Factories
  20. Standalone functions
  21. Utility types
  22. Behaviour notes
  23. Best practices

Optics

Type-safe, functional optics for immutable data in TypeScript.

  • Lens — total focus on a required value
  • Prism — partial focus on an optional or union value
  • Iso — total, invertible mapping between two types
  • Traversal — focus on zero or more values (e.g. all elements of an array)
  • Getter — read-only total focus (computed/derived values)
  • Fold — read-only multi-focus (extract without modify)

All optics compose freely via a standalone compose function. Four ergonomic combinators — guard, at, index, and each — cover the most common construction patterns.

Installation

# npm
npm install @fuiste/optics

# pnpm
pnpm add @fuiste/optics

# yarn
yarn add @fuiste/optics

# bun
bun add @fuiste/optics

Development

  • Package manager: pnpm
  • Supported Node.js: >=20.19.0

Quick start

Lens (required data)

import { Lens } from '@fuiste/optics'

type Person = {
  name: string
  age: number
  address: { street: string; city: string }
}

const nameLens = Lens<Person>().prop('name')

const person: Person = { name: 'John', age: 30, address: { street: '123', city: 'NYC' } }

nameLens.get(person) // 'John'
nameLens.set('Jane')(person) // { name: 'Jane', age: 30, address: { ... } }
nameLens.set((name) => name.toUpperCase())(person) // name == 'JOHN'

Prism (optional data)

import { Prism } from '@fuiste/optics'

type Person = {
  name: string
  address?: { street: string; city: string }
}

const addressPrism = Prism<Person>().of({
  get: (p) => p.address,
  set: (address) => (p) => ({ ...p, address }),
})

addressPrism.get({ name: 'A' }) // undefined
addressPrism.set({ street: '456', city: 'LA' })({ name: 'A' })
// => { name: 'A', address: { street: '456', city: 'LA' } }

// Functional updater — no-op when absent
addressPrism.set((addr) => ({ ...addr, city: 'LA' }))({
  name: 'A',
  address: { street: '1', city: 'NYC' },
})
// => { name: 'A', address: { street: '1', city: 'LA' } }

Iso (invertible mapping)

import { Iso } from '@fuiste/optics'

const numberString = Iso<number, string>({ to: (n) => `${n}`, from: (s) => parseInt(s, 10) })

numberString.to(42) // '42'
numberString.from('7') // 7

Traversal (multiple values)

import { Lens, compose, each } from '@fuiste/optics'

type Team = { members: string[] }

const membersLens = Lens<Team>().prop('members')
const eachMember = each<string>()

const allMembers = compose(membersLens, eachMember)

const team: Team = { members: ['Alice', 'Bob'] }

allMembers.getAll(team) // ['Alice', 'Bob']
allMembers.modify((n) => n.toUpperCase())(team)
// => { members: ['ALICE', 'BOB'] }

Getter (computed read-only value)

import { Lens, Getter, compose } from '@fuiste/optics'

type Person = { firstName: string; lastName: string }
type Team = { lead: Person }

const fullName = Getter<Person, string>((p) => `${p.firstName} ${p.lastName}`)
const leadLens = Lens<Team>().prop('lead')

const leadName = compose(leadLens, fullName)
leadName.get({ lead: { firstName: 'Alice', lastName: 'Smith' } }) // 'Alice Smith'
// leadName has no `set` — it's read-only

Fold (read-only extraction of many values)

import { Fold } from '@fuiste/optics'

const words = Fold<string, string>((s) => s.split(' '))
words.getAll('hello world') // ['hello', 'world']

Composition

All optics compose via the standalone compose(outer, inner) function. The return type is determined automatically:

outer ∖ innerLensPrismIsoTraversalGetterFold
LensLensPrismLensTraversalGetterFold
PrismPrismPrismPrismTraversalFoldFold
IsoLensPrismIsoTraversalGetterFold
TraversalTraversalTraversalTraversalTraversalFoldFold
GetterGetterFoldGetterFoldGetterFold
FoldFoldFoldFoldFoldFoldFold

Rules of thumb:

  • Anything with Fold → Fold (read-only is contagious)
  • Getter + partial optic → Fold; Getter + total optic → Getter
  • Traversal absorbs other writable optics → Traversal
  • Iso is transparent: the other optic's kind wins
  • Lens ∘ Lens → Lens; everything else with Prism → Prism
import { Lens, Prism, Iso, compose } from '@fuiste/optics'

type Address = { street: string; city: string }
type Person = { name: string; address?: Address }

const addressPrism = Prism<Person>().of({
  get: (p) => p.address,
  set: (address) => (p) => ({ ...p, address }),
})
const cityLens = Lens<Address>().prop('city')

// Prism ∘ Lens => Prism
const cityPrism = compose(addressPrism, cityLens)
cityPrism.get({ name: 'A', address: { street: '1', city: 'NYC' } }) // 'NYC'
cityPrism.get({ name: 'A' }) // undefined
cityPrism.set('LA')({ name: 'A' }) // unchanged (missing path is a no-op)

// Lens ∘ Iso => Lens
type Model = { count: number }
const countLens = Lens<Model>().prop('count')
const numberString = Iso<number, string>({ to: (n) => `${n}`, from: (s) => parseInt(s, 10) })
const countAsString = compose(countLens, numberString)
countAsString.get({ count: 7 }) // '7'
countAsString.set('10')({ count: 7 }) // { count: 10 }

Multi-step chains

Compose can be nested for deep paths:

import { Lens, compose, each } from '@fuiste/optics'

type Company = { employees: Array<{ name: string; role: string }> }

const employeesLens = Lens<Company>().prop('employees')
const eachEmployee = each<{ name: string; role: string }>()
const empName = Lens<{ name: string; role: string }>().prop('name')

const allEmployeeNames = compose(compose(employeesLens, eachEmployee), empName)

allEmployeeNames.getAll(company) // ['Alice', 'Bob', ...]
allEmployeeNames.modify((n) => n.toUpperCase())(company)

Combinators

guard — type-guard prism

Creates a prism from a TypeScript type guard. Much more ergonomic than manually writing Prism().of(...) for discriminated unions.

import { guard, Lens, compose } from '@fuiste/optics'

type Circle = { type: 'circle'; radius: number }
type Square = { type: 'square'; side: number }
type Shape = Circle | Square

const circlePrism = guard<Shape, Circle>((s): s is Circle => s.type === 'circle')

circlePrism.get({ type: 'circle', radius: 5 }) // { type: 'circle', radius: 5 }
circlePrism.get({ type: 'square', side: 4 }) // undefined

// Compose with a lens
const circleRadius = compose(circlePrism, Lens<Circle>().prop('radius'))
circleRadius.get({ type: 'circle', radius: 5 }) // 5
circleRadius.set(10)({ type: 'circle', radius: 5 }) // { type: 'circle', radius: 10 }

at — record key access

Creates a prism that focuses on a key in a Record<string, V>. Returns undefined when the key is absent; sets/upserts when called.

import { at, Lens, compose } from '@fuiste/optics'

type Config = { headers: Record<string, string> }

const headersLens = Lens<Config>().prop('headers')
const authHeader = at<string>('Authorization')

const configAuth = compose(headersLens, authHeader)

configAuth.get({ headers: { Authorization: 'Bearer x' } }) // 'Bearer x'
configAuth.get({ headers: {} }) // undefined
configAuth.set('Bearer y')({ headers: {} })
// => { headers: { Authorization: 'Bearer y' } }

index — array element access

Creates a prism that focuses on a single array element. get returns undefined when the index is out of bounds, and set is a no-op when the element is missing.

import { index } from '@fuiste/optics'

const second = index<number>(1)

second.get([10, 20, 30]) // 20
second.get([10]) // undefined
second.set(99)([10, 20, 30]) // [10, 99, 30]
second.set(99)([10]) // unchanged

each — array traversal

Creates a traversal over all elements of a ReadonlyArray<A>.

import { each, compose, Lens } from '@fuiste/optics'

const nums = each<number>()
nums.getAll([1, 2, 3]) // [1, 2, 3]
nums.modify((n) => n * 2)([1, 2, 3]) // [2, 4, 6]

API reference

Types

type Lens<S, A> = {
  _tag: 'lens'
  get: (s: S) => A
  set: (a: A | ((a: A) => A)) => (s: S) => S
}

type Prism<S, A> = {
  _tag: 'prism'
  get: (s: S) => A | undefined
  set: (a: A | ((a: A) => A)) => (s: S) => S
}

type Iso<S, A> = {
  _tag: 'iso'
  to: (s: S) => A
  from: (a: A) => S
}

type Traversal<S, A> = {
  _tag: 'traversal'
  getAll: (s: S) => ReadonlyArray<A>
  modify: (f: (a: A) => A) => (s: S) => S
}

type Getter<S, A> = {
  _tag: 'getter'
  get: (s: S) => A
}

type Fold<S, A> = {
  _tag: 'fold'
  getAll: (s: S) => ReadonlyArray<A>
}

type Optic<S, A> = Lens<S, A> | Prism<S, A> | Iso<S, A> | Traversal<S, A> | Getter<S, A> | Fold<S, A>

Factories

// Lens factory — use .prop to focus on a property key
Lens<S>().prop<K extends keyof S>(key: K): Lens<S, S[K]>

// Prism factory — use .of to build from get/set
Prism<S>().of<A>({ get, set }): Prism<S, A>

// Direct constructors
Iso<S, A>({ to, from }): Iso<S, A>
Traversal<S, A>({ getAll, modify }): Traversal<S, A>
Getter<S, A>(get: (s: S) => A): Getter<S, A>
Fold<S, A>(getAll: (s: S) => ReadonlyArray<A>): Fold<S, A>

Standalone functions

// Universal composition — 36 overloads, result type inferred from inputs
compose<S, A, B>(outer: Optic<S, A>, inner: Optic<A, B>): Optic<S, B>

// Type-guard prism
guard<S, A extends S>(predicate: (s: S) => s is A): Prism<S, A>

// Record key prism
at<V>(key: string): Prism<Record<string, V>, V>

// Array index prism
index<A>(idx: number): Prism<ReadonlyArray<A>, A>

// Array element traversal
each<A>(): Traversal<ReadonlyArray<A>, A>

Utility types

InferSource<O extends Optic>  // Extract the S from any optic
InferTarget<O extends Optic>  // Extract the A from any optic

Behaviour notes

  • Lens#set and Prism#set both accept a value or (a) => a updater function and return a new object. Originals are never mutated.
  • Writable optics preserve the caller's source shape in their public setter signatures for compatibility with narrowed states.
  • Prism#get may return undefined. In composed prisms, any missing outer branch results in undefined.
  • Prism#set through a composed path where an outer branch is missing is a no-op by default. Function updaters are also no-ops when missing.
  • Exception — Prism ∘ Iso: providing a concrete value materializes via the outer Prism's set even when get returns undefined, because the Iso can always construct the intermediate value. Function updaters remain a no-op when missing.
  • Traversal#modify applies the function to every focused element. For composed traversals through a missing prism branch, modify is a no-op.
  • Unchanged updates preserve reference identity whenever the library can detect that the focused value did not change.
  • Getter and Fold are read-only — they have no set or modify. Composing any optic with a read-only optic produces a read-only result.

Best practices

  • Prefer composition of small optics over one big custom getter/setter
  • Use guard for discriminated unions instead of manual Prism().of
  • Use each + compose for bulk array operations
  • Use index() for a single array element and at() for record/map keys
  • Use Getter for derived values that shouldn't be settable
  • Treat optics as pure: never mutate inputs inside set
  • For arrays, prefer each() for all elements or index(i) for one element. Lens<T[]>().prop(index) remains supported for compatibility.

版本与发布记录

@fuiste/optics 共发布 5 个版本,首次发布于 2025/09/19;过去 12 个月发布了 1 个版本。

版本发布日期距今
2.0.0latest2026/04/117 个月前
1.0.12025/09/241.1 年前
1.0.02025/09/191.2 年前
0.1.12025/09/191.2 年前
0.1.02025/09/191.2 年前

关于 @fuiste/optics 的常见问题

@fuiste/optics 是什么?

Type-safe, functional optics (lenses, prisms, traversals, and more) for immutable data

如何安装 @fuiste/optics?

在项目目录运行 npm install @fuiste/optics,或使用 pnpm add / yarn add / bun add。

@fuiste/optics 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@fuiste/optics 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@fuiste/optics 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20.19.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@fuiste/optics 可以商用吗?

@fuiste/optics 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@fuiste/optics 的下载量有多少?

根据 npm 官方下载统计,@fuiste/optics 在 2026/10/16 – 2026/11/14 这 30 天内被下载 7,274 次,日均约 242 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@fuiste/optics 还在维护吗?

最新版本 2.0.0 发布于 2026/04/11(7 个月前);至今共发布 5 个版本,其中过去 12 个月发布了 1 个;首次发布于 2025/09/19。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。