{"id":22093209,"url":"https://github.com/iadvize/opaque-union-library","last_synced_at":"2025-07-24T20:32:40.903Z","repository":{"id":36984066,"uuid":"273735186","full_name":"iadvize/opaque-union-library","owner":"iadvize","description":"@iadvize-oss/opaque-type  - opaque types union for Typescript","archived":false,"fork":false,"pushed_at":"2023-03-05T01:31:03.000Z","size":834,"stargazers_count":2,"open_issues_count":12,"forks_count":1,"subscribers_count":18,"default_branch":"master","last_synced_at":"2024-04-23T22:55:32.303Z","etag":null,"topics":["typescript"],"latest_commit_sha":null,"homepage":"https://iadvize.github.io/opaque-union-library/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/iadvize.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":".github/CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":".github/CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":"CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-06-20T15:34:06.000Z","updated_at":"2021-08-23T09:35:15.000Z","dependencies_parsed_at":"2024-10-04T11:27:17.000Z","dependency_job_id":"125eca51-1012-42f6-aa9e-f948a2f1cde8","html_url":"https://github.com/iadvize/opaque-union-library","commit_stats":{"total_commits":75,"total_committers":5,"mean_commits":15.0,"dds":0.24,"last_synced_commit":"f07d762e859cc930b96d6cb65b4ba2359e1236bf"},"previous_names":[],"tags_count":4,"template":false,"template_full_name":"iadvize/hello-world-javascript-library","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iadvize%2Fopaque-union-library","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iadvize%2Fopaque-union-library/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iadvize%2Fopaque-union-library/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iadvize%2Fopaque-union-library/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/iadvize","download_url":"https://codeload.github.com/iadvize/opaque-union-library/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":227476010,"owners_count":17779417,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["typescript"],"created_at":"2024-12-01T03:13:17.000Z","updated_at":"2024-12-01T03:13:17.691Z","avatar_url":"https://github.com/iadvize.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"@iadvize-oss/opaque-union library\n=============================\n![Continuous integration](https://github.com/iadvize/opaque-union-library/workflows/Continuous%20integration/badge.svg)\n\nThis experimental library provides helpers to create and maintain opaque domain\nsumtypes in Typescript.\n\nInspired by https://github.com/sledorze/morphic-ts/ and\nhttps://github.com/iadvize/opaque-type-library/.\n\n# Example\n\nLet's say your app deals with messages: \n\n```typescript\n// message.ts\n\ntype $Text = {\n  author: string;\n  content: string;\n}\n\ntype $Image = {\n  author: string;\n  source: string;\n  description: string;\n  mimetype: 'jpeg' | 'png',\n}\n\ntype $Video = {\n  author: string;\n  source: string;\n  description: string;\n  autoplay: boolean;\n}\n\ntype $Message = $Text | $Image | $Video;\n```\n\nThese are your private types (we like to prefix them with `$`). You don't want\nto expose them directly. You want to make them *opaque* for the rest of your app\nand expose an API to use them.\n\nLet's create the opaque union helper:\n\n```typescript\n// message.ts\n\nimport * as Union from '@iadvize-oss/opaque-union';\n\nconst MessageAPI = Union.of({\n  Text: Union.type\u003c$Text\u003e(),\n  Image: Union.type\u003c$Image\u003e(),\n  Video: Union.type\u003c$Video\u003e(),\n});\n\nexport type Text = ReturnType\u003ctypeof MessageAPI.of.Text\u003e; // Union.Opaque\u003c'Text'\u003e\nexport type Image = ReturnType\u003ctypeof MessageAPI.of.Image\u003e;\nexport type Video = ReturnType\u003ctypeof MessageAPI.of.Video\u003e;\n\n// this is a union of each opaque type\n// ie. Text | Image | Video\nexport type Message = Union.Type\u003ctypeof MessageAPI\u003e;\n```\n\nAn helper for media messages only will also be helpful.\n\n```typescript\n// message.ts\n\nimport * as Union from '@iadvize-oss/opaque-union';\n\nconst MediaMessageAPI = Union.omit(MessageAPI, ['Text']);\n```\n\nYou can now easily create the API you want your module to expose.\n\nFirst, constructors:\n\n```typescript\n// message.ts\n\nexport const createText = MessageAPI.of.Text; // (props: $Text) =\u003e Union.Opaque\u003c'Text'\u003e\nexport const createImage = MessageAPI.of.Image;\nexport const createVideo = MessageAPI.of.Video;\n\n```\n\nTo be used somewhere else in your app like that:\n\n```typescript\nimport * as Message from './path/to/message.ts';\n\nconst textMessage = Message.createText({ // textMessage is opaque\n  author: 'Jean',\n  content: 'Hello world!',\n});\n\nconst imageMessage = Message.createImage({\n  author: 'Peter',\n  source: 'http://...',\n  description: 'A goat',\n  mimetype: 'jpeg',\n});\n```\n\nWe can't work directly on the opaque `textMessage` or `imageMessage` variables\nbecause we can't see what's inside.\n\n```typescript\n// Error: Property 'author' does not exist on type 'Opaque\u003c\"Text\"\u003e'.\ntextMessage.author\n```\n\nYou're safe. Only `MessageAPI` knows how to \"unopaque\" these variables and work\non their content. It's best to keep the API private to your `message.ts` module.\n\nTo help you do just that, you can write properties accessors easily:\n\n```typescript\n// message.ts\n\nexport const author = MessageAPI.lensFromProp('author').get;\n\nexport const content = MessageAPI.Text.lensFromProp('content').get;\n\nexport const source = MediaMessageAPI.lensFromProp('source').get;\n\nexport const description = MediaMessageAPI.lensFromProp('description').get;\n\nexport const mimetype = MessageAPI.Image.lensFromProp('mimetype').get;\n\nexport const autoplay = MessageAPI.Video.lensFromProp('autoplay').get;\n```\n\nTo be used in another file like this: \n\n```typescript\nimport * as Message from './path/to/message.ts';\n\nconst textContent = Message.content(textMessage);\nconst imageDescription = Message.description(imageMessage);\n```\n\nYou can create more powerful and time saving compound accessors as well:\n\n```typescript\n// message.ts\n\nexport const summary = MessageAPI.fold({\n  Text: text =\u003e `${author(text)} send \"${content(text)}\"`,\n  Image: image =\u003e `${author(image)} send a ${mimetype(image)} image \"${description(image)}\"`,\n  Video: video =\u003e `${author(video)} send a video \"${description(video)}\"`,\n});\n```\n\nTo be used for example like that: \n\n```typescript\nimport * as Message from './path/to/message.ts';\n\nfunction log(message: Message.Message) {\n  console.log(Message.summary(message));\n}\n```\n\nYou can write transformations:\n\n```typescript\n// message.ts\n\nexport const addSignature = (signature: string) =\u003e MessageAPI.Text.iso.modify(\n  $text =\u003e ({ ...$text, content: `${$text.content}\\n${signature}` })\n);\n```\n\nIt's very composable. For example here with\n[fp-ts](https://gcanti.github.io/fp-ts/modules/) `pipe` function:\n\n```typescript\nimport { pipe } from 'fp-ts/es6/function';\nimport * as Message from './path/to/message.ts';\n\nconst signature = 'Jean (jean@email.com)';\n\nconst textMessageWithSignature = pipe(\n  Message.createText(...),\n  Message.addSignature(signature),\n);\n```\n\nAnd finally, the classic helpers you've come to expect come bundled in:\n\n```typescript\n// message.ts\n\nexport const isText = MessageAPI.is.Text;\nexport const fold = MessageAPI.fold;\n```\n\n```typescript\nimport * as Message from './path/to/message.ts';\n\nif (Message.isText(message)) {\n  return \u003cTextMessage message={message}\u003e\n} else {\n  return \u003cUnsupportedMessage \u003e\n}\n\n// or\n\nreturn pipe(\n  message,\n  Message.fold({\n    Text: text =\u003e \u003cTextMessage message={text} /\u003e,\n    Image: () =\u003e \u003cUnsupportedMessage /\u003e,\n    Video: () =\u003e \u003cUnsupportedMessage /\u003e,\n  }),\n);\n```\n\n# Advanced example\n\nWhat if a Message should be either `Pending` or `Sent`? This is what is called\n\"variation\" in the library.\n\n```typescript\n// message.ts\n\nimport * as Union from '@iadvize-oss/opaque-union';\n\nconst MessageAPI = Union.ofVariations({\n  Text: {\n    Pending: Union.type\u003c$Text\u003e(),\n    Sent: Union.type\u003c$Text\u003e(),\n  },\n  Image: {\n    Pending: Union.type\u003c$Image\u003e(),\n    Sent: Union.type\u003c$Image\u003e(),\n  },\n});\n\nconst pendingTextMessage = MessageAPI.of.Text.Pending({ ... });\n```\n\nUsing variations, you will be able to model your entities with a table, like\nbelow, while still using all the library union helpers.\n\n|         | Text | Image |\n|---------|------|-------|\n| Pending |      |       |\n| Sent    |      |       |\n\n\n# Install\n\n```\nnpm add @iadvize-oss/opaque-union\n```\n\n# Documentation\n\n[📖 Documentation](https://iadvize.github.io/opaque-union-library/)\n\n# Optics\n\nThe union API exposes some [monocle-ts](https://github.com/gcanti/monocle-ts)\noptics:\n\n## `Iso`\n\nAn `Iso` is a tool to transform between two types without any loss. In our case:\n\n```\n  Opaque\u003cKey\u003e ---\u003e Type (get, from, unwrap)\n  Opaque\u003cKey\u003e \u003c--- Type (reverseGet, to, wrap)\n```\n\nIt's particulary useful to transform the private value inside the opaque but you\ncan also combine it with other\n[monocle-ts](https://github.com/gcanti/monocle-ts) optics. \n\nUse `\u003cAPI\u003e.\u003cType\u003e.iso` to have full control over one type and its corresponding\nopaque type.\n\n```typescript\nconst iso = MessageAPI.Text.iso; // Iso\u003cOpaque\u003cType\u003e, Type\u003e\n\nconst fromOpaque: (text: Text) =\u003e $Text = iso.from;\nconst toOpaque: ($text: $Text) =\u003e Text = iso.to;\n```\n\nTo be used like this, when you need to \"unopaque\" your type in a private module\nfunction, for example:\n\n```typescript\nfunction translate(textMessage: Text): Text { \n  const privateContent = fromOpaque(\n    textMessage, // this is an Opaque\u003c'Text'\u003e\n  ); // \"hello world\"\n    \n  const translation = translateText(privateContent); // some magic here\n\n  const opaqueTextMessageAgain = toOpaque(\n    translation, // this is a $Text\n  ); // Opaque\u003c'Text'\u003e\n\n  return opaqueTextMessageAgain\n}\n```\n\nYou can also use the `Iso` directly for transformations:\n\n```typescript\nconst iso = MessageAPI.Text.iso; // Iso\u003cOpaque\u003cType\u003e, Type\u003e\n\nconst addSignature = (signature: string) =\u003e iso.modify(\n  // you have access to the private type here\n  $text =\u003e `${$text}\\n${signature}`,\n);\n```\n\nTo be used like this:\n\n```typescript\nconst textMessage: Text = ...;\n\nconst textMessageWithSignature = addSignature('Jean (jean@email.com)')(textMessage);\n```\n\nThere is also a global `Iso` exposed on `\u003cAPI\u003e.iso` to switch between any opaque\nand any private types. \n\n```typescript\nconst iso: Iso\u003c\n  Message, // the opaque union\n  Union.Tagged\u003c$Computer, 'Computer'\u003e | Union.Tagged\u003c$Smartphone, 'Smartphone'\u003e | Union.Tagged\u003c$Smartphone, 'Smartphone'\u003e\n\u003e = MessageAPI.iso;\n```\n\nWhere `type Tagged\u003cT, Name\u003e = T \u0026 { _key: Name }` is used to not lose the type\nof the entity when switching from an opaque to the corresponding private type.\nThat's why the global `.iso` is restricted to members of the union that are\nassignabled to `object`.\n\n## `lensFromProp`\n\nA [monocle-ts](https://github.com/gcanti/monocle-ts) `Lens` is a tool to\ntransform between a type and a subtype of it.\n\nUse the global `\u003cAPI\u003e.lensFromProp` to create a `Lens` between any opaque of the\nAPI and a property shared by all private types of the API (if any).\n\nIn your module:\n\n```typescript\n// media.ts\ntype $Image = {\n  source: string;\n}\n\ntype $Video = {\n  source: string;\n  autoplay: boolean;\n}\n\nconst MediaAPI = Union.of({\n  Image: Union.type\u003c$Image\u003e(),\n  Video: Union.type\u003c$Video\u003e(),\n});\n\nexport const createImage = MessageAPI.of.Image;\nexport const createVideo = MessageAPI.of.Video;\n\nexport const source = MediaAPI.lensFromProp('source').get;\nexport const updateSource = MediaAPI.lensFromProp('source').modify;\n```\n\nSomewhere else in your app:\n\n```typescript\nimport * as Media from './path/to/media.ts';\n\nconst image = Media.createImage({ source: 'http://a.com/b.jpeg ' });\n\nconsole.log('source: ', source(image)); // http://a.com/b.jpeg\n\nconst newImage = Media.updateSource(\n  oldSource =\u003e oldSource.replace('a.com', 'static.a.com')\n)(image);\n```\n\nUse `\u003cAPI\u003e.\u003cType\u003e.lensFromProp` to create an `Lens\u003cOpaque\u003cType\u003e, Type[...]`.\n\n```typescript\nconst videoMessage: Video = ...;\n\nconst autoplay = MediaAPI.Video.lensFromProp('autoplay').get;\nconst removeAutoPlay = MediaAPI.Video.lensFromProp('autoplay').set(false);\n```\n\nSomewhere else in your app:\n\n```typescript\nimport * as Media from './path/to/media.ts';\n\nconst video = Media.createImage({ source: 'http://a.com/b.jpeg ', autoplay: true });\n\nconst videoWithoutAutoplay = Media.removeAutoPlay(video);\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fiadvize%2Fopaque-union-library","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fiadvize%2Fopaque-union-library","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fiadvize%2Fopaque-union-library/lists"}