Git Product home page Git Product logo

optics-ts's Introduction

optics-ts

Build

optics-ts provides type-safe, ergonomic, polymorphic optics for TypeScript:

  • Optics allow you to read or modify values from deeply nested data structures, while keeping all data immutable.
  • Ergonomic: Optics are composed with method chaining, making it easy and fun!
  • Polymorphic: When writing through the optics, you can change the data types in the nested structure.
  • Type-safe: The compiler will type check all operations you do. No any, ever.

Documentation

Features

optics-ts supports lenses, prisms, traversals, removing items from containers, and much more!

Since optics-ts v2.2.0, there are two syntaxes for defining optics: method chaining (the default) and standalone optics (experimental). See the docs for more info!

Getting started

Installation:

npm install optics-ts

or

yarn add optics-ts

Here's a simple example demonstrating how lenses can be used to drill into a nested data structure:

import * as O from 'optics-ts'

type Book = {
  title: string
  isbn: string
  author: {
    name: string
  }
}

// Create a lens that focuses on author.name
const optic = O.optic_<Book>().prop('author').prop('name')

// This is the input data
const input: Book = {
  title: "The Hitchhiker's Guide to the Galaxy",
  isbn: '978-0345391803',
  author: {
    name: 'Douglas Adams',
  },
}

// Read through the optic
O.get(optic)(input)
// "Douglas Adams"

// Write through the optic
O.set(optic)('Arthur Dent')(input)
// {
//   title: "The Hitchhiker’s Guide to the Galaxy"
//   isbn: "978-0345391803",
//   author: {
//     name: "Arthur Dent"
//   }
// }

// Update the existing value through the optic, while also changing the data type
O.modify(optic)((str) => str.length + 29)(input)
// {
//   title: "The Hitchhiker’s Guide to the Galaxy"
//   isbn: "978-0345391803",
//   author: {
//     name: 42
//   }
// }

Another example that converts all words longer than 5 characters to upper case:

import * as O from 'optics-ts/standalone'

const optic = O.optic<string>().words().when(s => s.length >= 5)

const input = 'This is a string with some shorter and some longer words'
O.modify(optic)((s) => s.toUpperCase()(input)
// "This is a STRING with some SHORTER and some LONGER WORDS"

See the documentation for a tutorial and a detailed reference of all supported optics.

Development

Run yarn to install dependencies.

Running the test suite

Run yarn test.

For compiling and running the tests when files change, run these commands in separate terminals:

yarn build:test --watch
yarn jest dist-test/ --watchAll

Documentation

You need Python 3 to build the docs.

python3 -m venv venv
./venv/bin/pip install mkdocs-material

Run a live reloading server for the documentation:

./venv/bin/mkdocs serve

Open http://localhost:8000/ in the browser.

Releasing

$ yarn version --new-version <major|minor|patch>
$ yarn publish
$ git push origin main --tags

Open https://github.com/akheron/optics-ts/releases, edit the draft release, select the newest version tag, adjust the description as needed.

optics-ts's People

Contributors

akheron avatar cristatus avatar dependabot[bot] avatar ericcrosson avatar mtreinik avatar polemius avatar valyagolev avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

optics-ts's Issues

How to traverse `Record` types?

Is there the equivalent of the array value elems() traversal for Record values? I'm thinking things like keys(), values(), and entries().

Also, if you want to compose optics that are not included in the builder syntax is there an "escape hatch"? In other words, how would I define a values() traversal in user-land that didn't require a change to optics-ts itself?

at(index)-lens for tuples

Heya, consider the case:

optic<[number, string]>().index(0) 

This produces a Prism, but we know that this is a tuple, and would make more sense for it to produce a lens (since there's no doubt that the item at index 0 for the tuple is a number).

Is there something I've missed here? It's important for me to be able to focus on a tuple and for that optic to be a lens and not a prism.

Does not type check correctly when loading from Deno

the following script:

import * as O from "npm:optics-ts/standalone"
O.prop('hi');

does not type check in Deno. It fails with the following error:

error: Uncaught Error: Error resolving package config for 'npm:optics-ts/standalone'.: [ERR_INVALID_PACKAGE_TARGET] Invalid "exports" target {"import":"./dist/mjs/standalone/index.js","require":"./dist/cjs/standalone/index.js"} defined for './standalone' in the package config /Users/cowboyd/Library/Caches/deno/npm/registry.npmjs.org/optics-ts/2.4.0package.json imported from file:///Users/cowboyd/Library/Caches/deno/npm/registry.npmjs.org/optics-ts/2.4.0/; target must start with "./"

Something like valueOr but for null?

valueOr works well for | undefined fields, but not for | null fields. I didn't see any function to support this, but maybe I overlooked something?

import * as O from 'optics-ts';

interface ZUndef {
  a?: AUndef;
}

interface AUndef {
  b?: BUndef;
}

interface BUndef {
  x: number;
}

const testUndef = () => {
  const defaultA: AUndef = {};
  const defaultB: BUndef = {x: 7};

  const xO = O.optic<ZUndef>().prop('a').valueOr(defaultA).prop('b').valueOr(defaultB).prop('x');

  const data: ZUndef = {};

  const res = O.set(xO)(11)(data);

  console.log('undef', res); // ok: undef { a: { b: { x: 11 } } }
};

testUndef();

// ---

interface ZNull {
  a: ANull | null;
}

interface ANull {
  b: BNull | null;
}

interface BNull {
  x: number;
}

const testNull = () => {

  const defaultA: ANull = {b: null};
  const defaultB: BNull = {x: 7};

  const xO = O.optic<ZNull>().prop('a').valueOr(defaultA).prop('b').valueOr(defaultB).prop('x'); // TS2345: Argument of type 'string' is not assignable to parameter of type 'never'.

  const data: ZNull = {a: null};

  const res = O.set(xO)(11)(data); // TypeError: Cannot read property 'b' of null

  console.log('null', res);
};

testNull();

a pick using optics

would it be possible to create a pick that allow to use existing optic to collect the keys:

const user = { firstName: 't', lastName: 't', age: 1}

O.get(Opick(lFirstName, lLastname), user) // return =>  { firstName: 't', lastName: 't' }

maybe it's already possible or we need something more generic like a parallel where the optics are all run on the source object instead of piped?

API to define recursive traversals

Hi There,

I'm loving this library, and I'm trying to explore all the ways in which I could use it.

I want to do something similar to what's described here https://www.michaelpj.com/blog/2020/08/02/lenses-for-tree-traversals.html where you can treat a recursive substructure as a simple traversal.

For example, let's say I wanted to get all the input elements in a DOM as a flat list. How would I write this custom traversal?

import { collect } from 'optics-ts';
import { inputs } from 'my-dom-library';

collect(inputs)(document.documentElement) //=> HTMLInputElement[]

(I know that the DOM isn't an immutable structure, but it's the first example that came to me.)

`remove` set object key to Symbol(__remove_me__) and break Jest `toEqual`

When use Jest to test object manipulation with optics-ts, Jest reported that remove set object key to the Symbol(__remove_me__) so test failed.

JSON.stringify wouldn't print object key with the Symbol but Jest's toEqual would report error when compare it to object without the key.

Is it expected or a bug? I did a search and didn't find a well known symbol for remove me. Is it new standard?

”valueOr”, but for a whole prism.

Hi!

In the following code, value becomes type number | undefined, I understand that it’s because the valueOr is ”applied” to the last ’a’-prop, however I’d like the valueOr to ”be applied” to the whole lens.

  import { optic, preview } from 'optics-ts'                                                                             
                                                                                                                              
  type A =                                                                                                                    
    | {                                                                                                                       
        a?: number                                                                                                            
      }                                                                               
    | undefined                                                                       
                                                                                                                   
  const hello = optic<A>()                                                           
    .optional()                                                                      
    .prop('a')                                                                       
    .valueOr(5)                                                                      
                                                                                                                            
  const value = preview(hello)(undefined)                                                                                                                                                                            

Is there any way to accomplish this?

Errors with typeeerror (incorrect NotAnArrayType)

Heya,

Thanks for this lib, I love it.

In my repo:
https://github.com/merisbahti/io-ts-experiment/blob/master/extract-errors-with-path.test.ts

I copied one of your tests, (see describe(’lens/filter’))
And I get this error:

extract-errors-with-path.test.ts:20:26 - error TS2339: Property 'bar' does not exist on type 'NotAnArrayType<Source[]>'.
20     .filter(item => item.bar !== 'quux')

(in my repo, I just run npx tsc extract-errors-with-path.test.ts

And I’m not sure why I get it, we even share the exact same ts version.

Do you have any wisdom to share here? thanks

Setting property with value of empty object within list should not be undefined

I created the reproducer in jest below to should what I want to achieve using v2.3.0:

    describe("reproducer", () => {
        interface Data {
            id: number;
            columns: Column[];
        }
        interface Column {
            value?: string | undefined;
        }
        const columnByIndex_ = (index: number) => O.optic<Data>().prop("columns").optional().at(index);
        const value_ = O.optic<Column>().prop("value").optional();

        it("add property to column", () => {
            const data: Data = { id: 1, columns: [{}] };
            const value = "value";

            const result = O.set(columnByIndex_(0).compose(value_))(value)(data);

            expect(result).toStrictEqual({ ...data, columns: [{ value }] });
        });
    });

However, the test fails with

Error: expect(received).toStrictEqual(expected) // deep equality

- Expected  - 1
+ Received  + 1

  Object {
    "columns": Array [
      Object {
-       "value": "value",
+       "value": undefined,
      },
    ],
    "id": 1,
  }

Workaround:

O.modify(columnByIndex_(0))((col) => ({ ...col, value }))(data);

Notes:

  • It does not matter how many properties already contained in the object itself.
  • Same behavior appears using modify instead of set

Error: Unable to resolve

My expo/react-native project can't load the module optics-ts/standalone

Unable to resolve "optics-ts/standalone" from "src/utils/lenses/user.ts"

optics-ts seems to work fine just an issue with standalone.

when i import import * as O from "optics-ts/dist/mjs/standalone". vscode can't find the types but i get no error from metro on bundle. but he can't find internals so it's still not usable.

The project is in a monorepo in case that's a problem. the typings in vscode are found and i don't get an error on the import, the error is only when metro tries to bundle.

the tsconfig has "strict": true,

tsconfig:

{
	"extends": "expo/tsconfig.base",
	"compilerOptions": {
		"strict": true,
		"baseUrl": "./",
		"paths": {
			"@gg/assets/*": ["./assets/*"],
			"@gg/*": ["./src/*"],
			"*": ["*", "*.ios", "*.android"]
		}
	},
	"exclude": ["scripts"]
}

Support JSON-Pointer RFC

Maybe I'm misunderstanding as I'm still learning the library with Jotai, but it seems like it might be possible to use the JSON Pointer RFC standard to focus the optics on a path, in addition to the existing optics-ts syntax. What do you think?

I find JSON-Pointer useful with same-type-recursive-referential tree-like data structures which I haven't figured out how to deeply use in optics-ts yet.

Here are a few reference implementations:

TS is complaining when using Traversal

Hello !
First I'd like to thank you for this lib, this is amazing how many problems it solves !
But I'm facing an issue with Traversal though:

import * as O from 'optics-ts';

type Person = {
    name: string;
    friends: Person[];
};

const friendsNames = O.optic<Person>().prop('friends').elems().prop('name');

Whenever I try to use elems, TS is throwing an error : Argument of type '"name"' is not assignable to parameter of type 'keyof NotAnArrayType<Person[]>'.ts(2345)

I'm using optics-ts with TypeScript 4.8.3

expose lens constructor?

Stuck wanting a function to construct my own lens(). The reason for it is that I'm trying to use optics-ts with es Map, because basically I need an object that preserves item ordering. It requires using .get() and .set() to get/set entries; object syntax doesn't work. Unfortunately I can't figure out how to lens into it.

`modify` does not work with `guard` prisms against union types when transforming focus type

import * as O from 'optics-ts/standalone';

type Union = Plain | WithProp;
type Plain = {
  type: 'plain',
};
type WithProp = {
  type: 'with_prop',
  prop: string,
};

const isWithProp = (u: Union): u is WithProp => u.type === 'with_prop';
const isWithProp_guard = O.guard(isWithProp);
const withProps_prop = O.compose(isWithProp_guard, 'prop');

const plain: Union = {
  type: 'plain',
};

const withProp: Union = {
  type: 'with_prop',
  prop: 'the prop',
}

// (prop: string) => string
const modify = O.modify(withProps_prop, (prop: string): string => '1');

const modified_plain = modify(plain);
const modified_withProp = modify(withProp);

assert.strictEqual(modified_plain.type, 'plain');
assert.strictEqual(modified_withProp.type, 'with_prop');
assert.strictEqual(modified_withProp.type === 'with_prop' ? modified_withProp.prop : null, '1');

// (prop: string) => number
const transform = O.modify(withProps_prop, (prop: string): number => 1);

const transformed_plain = transform(plain);
const transformed_withProp = transform(withProp);

// type error from this point forward
assert.strictEqual(transformed_plain.type, 'plain');
assert.strictEqual(transformed_withProp.type, 'with_prop');
assert.strictEqual(transformed_withProp.type === 'with_prop' ? transformed_withProp.prop : null, 1);

[Feature] Data-first piping?

Here is a suggestion for a slight change to the API.

The current example from the Readme, as it is:

import * as O from 'optics-ts'

type Book = {
  title: string
  isbn: string
  author: {
    name: string
  }
}
const optic = O.optic_<Book>().prop('author').prop('name')

const input: Book = {
  title: "The Hitchhiker's Guide to the Galaxy",
  isbn: '978-0345391803',
  author: {
    name: 'Douglas Adams',
  },
}

O.get(optic)(input) // => "Douglas Adams"

O.set(optic)('Arthur Dent')(input)

O.modify(optic)((str) => str.length + 29)(input)

This is what it could read like, with a data-first API (like ReScript and the excellent ts-belt lib by @mobily does):

import * as O from 'optics-ts'

type Book = {
  title: string
  isbn: string
  author: {
    name: string
  }
}
const authorNameOf = O.optic_<Book>().prop('author').prop('name')

const book: Book = {
  title: "The Hitchhiker's Guide to the Galaxy",
  isbn: '978-0345391803',
  author: {
    name: 'Douglas Adams',
  },
}

O.get(authorNameOf)(book) // => "Douglas Adams"

O.set(authorNameOf)(book)('Arthur Dent')

O.modify(authorNameOf)(book)((str) => str.length + 29)

Imho, such an API would make it even more readable and approachable for newcomers.

Would such a data-first API be possible?

Maybe a better solution for this types on valueOr and optional?

started to use optics for work and i run into an issue with type and i'm not sure if i just use optics badly or not.

i have a record of type IUser and i created the optics for it (small sample):

export const lInitialPayment = O.compose("initialPayment", O.optional)
export const lInitialPaymentStatus = O.compose(lInitialPayment, O.optional, "status")

initialPayment can be undefined as well as initialPayment.status

ideally i would use valueOr to defined a default value

export const lInitialPaymentStatus = O.compose(lInitialPayment, "status", valueOr(EInitialPaymentStatus.PROCESSING)

but doing that doesn't send the type i was expecting. EInitialPaymentStatus but instead sends EInitialPaymentStatus | undefined

i'm guessing it's because lInitialPayment has optional and i'd rather not have a mix of Lens and Prism since it would require using get or preview which is annoying to expect to know for new dev and also makes it really hard to make helpers function like exist: (optic, data) => boolean since i wouldn't know if have to use get or preview of the optic as far as i know.

To avoid having to think which one to use, everything is a prism. + my user can have all his value undefined pretty much outside of like 3 fields so it makes sense to have everything optional but then valueOr should remove the undefined from the type from what i understand it does.

Looking at the types i managed to make a getOr function that return the right types (the one i expect at least):

export function getOr<
	C extends "Prism" | "Traversal" | "AffineFold" | "Fold" = "Prism",
	A extends HKT = any,
	S = any,
	D = any,
>(optic: Optic<C, A, any, any>, data: S, defaultValue: D) {
	return O.preview(O.compose(optic, O.valueOr(defaultValue)), data) as Apply<A, S> extends infer AU
		? AU
		: never
}

Just wanted to know if i do something really bad with optics to get this result or if it could makes sense to look into adapting the current types to match this behavior :)

Release rewrite/reread optics

Hey,

How about a release so that we can enjoy the rewrite/reread optics constructors?

Thanks a lot for this library, no stress.

Support readonly arrays

Hi! 👋

Firstly, thanks for your work on this project! 🙂

Today I used patch-package to patch [email protected] for the project I'm working on.

Here is the diff that solved my problem:

diff --git a/node_modules/optics-ts/utils.d.ts b/node_modules/optics-ts/utils.d.ts
index 75da246..c769d97 100644
--- a/node_modules/optics-ts/utils.d.ts
+++ b/node_modules/optics-ts/utils.d.ts
@@ -2,7 +2,7 @@ export interface NotAnArrayType<T> {
     readonly _: unique symbol;
     readonly _t: T;
 }
-export type ElemType<A> = IfElse<IsOptional<A>, NotAnArrayType<A>, A extends (infer Item)[] ? Item : NotAnArrayType<A>>;
+export type ElemType<A> = IfElse<IsOptional<A>, NotAnArrayType<A>, A extends (infer Item)[] ? Item : A extends readonly (infer Item)[] ? Item : NotAnArrayType<A>>;
 export type Eq<A, B> = [A, B] extends [B, A] ? true : false;
 export type Simplify<A, B> = Eq<A, B> extends true ? A : B;
 export type IsOptional<A> = Or<ExtendsUndefined<A>, ExtendsNull<A>>;

This issue body was partially generated by patch-package.

A benchmark of optics-ts, partial.lenses, and monocle-ts

Hi, thanks for this awesome library.

I did some benchmark as the title said. In summary optics-ts performance is the best in general. But its read is behind partial.lens. Is there room for further improvement?

Detail

I need to pick a JS/TS optics library for a project. After some initial research, I narrowed down to three candidates: partial.lenses, monocle-ts, optics-ts.

As my project is going to manipulate dataset with certain size, I ran some benchmarks, on four operations I cared the most:

  • read by lens
  • modify by lens
  • read array element by predicate
  • modify array element by predicate

Jest run result

Edit:
As the test data's array element's id is unique, I used find instead of elems to run again. The performance of both read and write was improved.
Here is the new test resut:

Use find

  read
    ✓ optics-ts (4ms)
    ✓ monocle-ts (1ms)
    ✓ partial.lenses (3ms)
  write
    ✓ optics-ts (3ms)
    ✓ monocle-ts (4ms)
    ✓ partial.lenses (16ms)
  prism read array element by predicate
    ✓ optics-ts (36ms)
    ✓ monocle-ts (219ms)
    ✓ partial.lenses (6ms)
  prism modify array element by predicate
    ✓ optics-ts (87ms)
    ✓ monocle-ts (30524ms)
    ✓ partial.lenses (338ms)

Use elems

  read
    ✓ optics-ts (4ms)
    ✓ monocle-ts (1ms)
    ✓ partial.lenses (3ms)
  write
    ✓ optics-ts (3ms)
    ✓ monocle-ts (4ms)
    ✓ partial.lenses (13ms)
  prism read array element by predicate
    ✓ optics-ts (70ms)
    ✓ monocle-ts (220ms)
    ✓ partial.lenses (6ms)
  prism modify array element by predicate
    ✓ optics-ts (214ms)
    ✓ monocle-ts (31227ms)
    ✓ partial.lenses (344ms)

It seems optics-ts has the best overall performance. I inclined to use it for my project.

Optics-ts is strong in write while partial.lenses is strong in read. I am not sure if there is still room to improve your read to partial.lenses' level..

Here is the benchmark repository.

js-optics-benchmark

p.s. just realized you are the maintainer of jansson. Now it is clear I will go with optics-ts.

:)

Setting a Prism should always apply no?

I'm not sure how it's suppose to work in Optics/lens land but i would expect that when i set a prop or deep prop, it gets set even when it's not yet defined.

import * as assert from 'assert'
import * as O from 'optics-ts/standalone'
import { pipe } from 'fp-ts/function'
import { some, none } from 'fp-ts/Option'


interface Company {
  name?: string
  address?: string
}
interface Employee {
  name?: string
  company?: Company
}

const emp: Employee= {
  name:'test',
  company:{
    address:'te'
  }
}

const noCompany:Employee: { name:'test2' }

const lName = O.compose('name', O.optional)
const lCName = O.compose('company', O.optional, 'name', O.optional)

// Would expect to set company.name = 'test23' and not just do nothing
O.set(lCName, 'test23', emp) 
// Would expect to set company.name = 'test23' and not just do nothing
O.set(lCName, 'test23', noCompany) 

Is there a method to do this in optics?
I use lenses to make it easy to set/update value deep in an object and create easy to use pre-build lens to access/modify data in complex object but with this behavior i can't really do that without expecting every dev to deeply know the object structure and how optics-ts works with valueOr / optional / Prism / etc

Possible to get the reverse of pick, like omit

Was looking for a way to get a partial object by removing some keys. In my case, omit would be 2 keys but pick would be like 20 so it's a lot harder to read and use and means i need to update it everytime i add/remove keys where omit would probably not change until i touch the exact keys which are specific to that function.

const data = {
  name: 'test',
  value: 10,
  isHere: true
}

O.get(O.omit('value'), data) // {name:'test', isHere: true}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.