{"id":17771330,"url":"https://github.com/lue-bird/elm-typed-value","last_synced_at":"2025-07-27T01:08:23.632Z","repository":{"id":52681584,"uuid":"356880680","full_name":"lue-bird/elm-typed-value","owner":"lue-bird","description":"a type with 1 variant but convenient","archived":false,"fork":false,"pushed_at":"2023-01-02T19:11:07.000Z","size":161,"stargazers_count":3,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-07-15T08:53:51.272Z","etag":null,"topics":["custom-type","elm","newtype","single-variant","tagged","wrapper"],"latest_commit_sha":null,"homepage":"https://package.elm-lang.org/packages/lue-bird/elm-typed-value/latest/","language":"Elm","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/lue-bird.png","metadata":{"files":{"readme":"README.md","changelog":"changes.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-04-11T13:50:51.000Z","updated_at":"2023-02-20T22:51:12.000Z","dependencies_parsed_at":"2023-02-01T03:45:20.705Z","dependency_job_id":null,"html_url":"https://github.com/lue-bird/elm-typed-value","commit_stats":null,"previous_names":[],"tags_count":17,"template":false,"template_full_name":null,"purl":"pkg:github/lue-bird/elm-typed-value","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lue-bird%2Felm-typed-value","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lue-bird%2Felm-typed-value/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lue-bird%2Felm-typed-value/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lue-bird%2Felm-typed-value/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lue-bird","download_url":"https://codeload.github.com/lue-bird/elm-typed-value/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lue-bird%2Felm-typed-value/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":267278915,"owners_count":24063279,"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","status":"online","status_checked_at":"2025-07-26T02:00:08.937Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["custom-type","elm","newtype","single-variant","tagged","wrapper"],"created_at":"2024-10-26T21:31:43.838Z","updated_at":"2025-07-27T01:08:23.589Z","avatar_url":"https://github.com/lue-bird.png","language":"Elm","funding_links":[],"categories":[],"sub_categories":[],"readme":"# [elm-typed-value](https://dark.elm.dmy.fr/packages/lue-bird/elm-typed-value/latest/)\n\nFor a `type` with just 1 variant,\n[`Typed`](Typed#Typed) is a convenient, safe replacement (↑ [limits](#limits))\n\nAttach a tag\nso a `Typed .. Meters .. Float` isn't accepted as `Typed .. Kilos .. Float` anymore.\n\n`type` boilerplate ↓ is covered by [`Typed`](Typed#Typed)\n```elm\nquantity =\n    \\(Meters quantity) -\u003e\n        quantity\n\nalter quantityAlter =\n    \\(Meters quantity) -\u003e\n        quantity |\u003e quantityAlter |\u003e Meters\n```\nand other helpers like mapping multiple etc.\n\n\nPlus you don't have to spell out the obvious:\n```elm\n3.2 |\u003e Meters.fromFloat\n\nprime |\u003e Prime.toInt\nheight |\u003e Meters.toFloat\n\n(oneHeight |\u003e Meters.toFloat)\n    + (otherHeight |\u003e Meters.toFloat)\n    |\u003e Meters.fromFloat\n```\n\nwith [`Typed`](Typed#Typed)\n\n```elm\n3.2 |\u003e tag Meters\n\nprime |\u003e untag\nheight |\u003e untag\n\noneHeight\n    |\u003e Typed.and otherHeight\n    |\u003e Typed.map (\\( h0, h1 ) -\u003e h0 + h1)\n```\n\n# Kinds of [`Typed`](Typed#Typed)\n\n  - [`Tagged`](Typed#Tagged) → attach a label things like when you'd use\n    ```elm\n    -- module Cat exposing (Cat(..))\n\n    type Cat\n        = -- variant can be used anywhere\n          Cat { name : String, mood : Mood }\n    ```\n    Users can create \u0026 alter new `Cat`s everywhere\n\n    A `type(..)` can't expose the variant for creating \u0026 altering without allowing access as well.\n    [`Typed`](Typed#Typed) can, as we'll see in [section `Tagged Internal`](#combined-with-tagged-internal)\n\n  - [`Checked`](Typed#Checked) → only \"validated\" things like when you'd use\n    ```elm\n    -- module Prime exposing (Prime)\n\n    type Prime\n        = -- nobody outside this module can use this variant\n          Prime Int\n    ```\n    creating \u0026 altering `Prime`s will only be possible inside that `module`\n\n    An opaque `type` can't expose the variant for destructuring only.\n    [`Typed`](Typed#Typed) can, as we'll see in [section `Checked Public`](#checked-public)\n\n  - [`Public`](Typed#Public) → everyone can access → [`untag`](Typed#untag)\n\n  - [`Internal`](Typed#Internal)\n    → only those with the tag can access → [`internal`](Typed#internal)\n\n## [`Tagged`](Typed#Tagged) [`Public`](Typed#Public)\n\n```elm\nimport Typed exposing (Typed, Tagged, Public, tag)\n\ntype alias Cat =\n    Typed Tagged CatTag Public { name : String, mood : Mood, napsPerDay : Float }\n\ntype CatTag\n    = Cat\n\ntype alias Dog =\n    Typed Tagged DogTag Public { name : String, mood : Mood, barksPerDay : Float }\n\ntype DogTag\n    = Dog\n\nsit : Dog -\u003e Dog\nsit =\n    Typed.map (\\d -\u003e { d | mood = Neutral })\n\nhowdy : Cat\nhowdy =\n    { name = \"Howdy\", mood = Happy, napsPerDay = 2.2 }\n        |\u003e tag Cat\n\nhowdy |\u003e sit -- error\n```\n\nAnother example:\n\n```elm \n-- module Pixels exposing (Pixels, PixelsTag(..))\n\nimport Typed exposing (Typed, Tagged, Public, tag)\n\ntype alias Pixels =\n    Typed Tagged PixelsTag Public Int\n\ntype PixelsTag\n    = Pixels\n\n-- in another module using Pixels\n\ninnerWidth : Pixels\ninnerWidth =\n    700 |\u003e tag Pixels\n\nborderWidth : Pixels\nborderWidth =\n    5 |\u003e tag Pixels\n\ndefaultWidth : Pixels\ndefaultWidth =\n    innerWidth\n        |\u003e Typed.and borderWidth\n        |\u003e Typed.map\n            (\\( inner, border ) -\u003e inner + border * 2)\n\ndefaultWidth |\u003e Typed.untag\n--\u003e 710\n```\n\n## [`Checked`](Typed#Checked) [`Public`](Typed#Public)\n\n```elm\n-- module Even exposing (Even, n0, n2, add, multiplyBy)\n\nimport Typed exposing (Typed, Checked, Public, tag)\n\n\ntype alias Even =\n    Typed Checked EvenTag Public Int\n\n\n-- don't expose(..) its variant\ntype EvenTag\n    = Even\n\n\nmultiplyBy : Int -\u003e Even -\u003e Even\nmultiplyBy factor =\n    \\even -\u003e\n        even\n            |\u003e Typed.map (\\int -\u003e int * factor)\n            |\u003e Typed.toChecked Even\n\n\nadd : Even -\u003e Even -\u003e Even\nadd toAddEven =\n    \\even -\u003e\n        even\n            |\u003e Typed.and toAddEven\n            |\u003e Typed.map\n                (\\( int, toAddInt ) -\u003e int + toAddInt)\n            |\u003e Typed.toChecked Even\n\n\nn0 : Even\nn0 =\n    0 |\u003e tag Even\n\n\nn2 : Even\nn2 =\n    2 |\u003e tag Even\n\n-- in another module using Even\n\ncakeForEven : Even -\u003e { cake : () }\ncakeForEven _ =\n    { cake = () }\n\nn0 |\u003e Typed.map (\\n -\u003e n + 1) |\u003e cakeForEven\n--→ compile-time error: is Tagged but expected Checked\n\nn2 |\u003e multiplyBy -5 |\u003e cakeForEven\n--\u003e { cake = () }\n```\nAbove example is just for illustration! In practice, [prefer a narrow type](#always-prefer-narrow-type-over-checked)\n```elm\ntype Even\n    = Times2 Int\n```\n\n## [`Checked`](Typed#Checked) [`Internal`](Typed#Internal)\n\nA validated thing that can't be directly accessed by a user.\n\nA module that only exposes randomly generated unique `Id`s:\n\n```elm\n-- module Id exposing (Id, random, toBytes, toString)\n\nimport Typed exposing (Typed, Checked, Internal, tag)\nimport Random\n\ntype alias Id =\n    Typed Checked IdTag Internal (List Int)\n\ntype IdTag\n    = Id\n\nrandom : Random.Generator Id\nrandom =\n    Random.list 4\n        (Random.int 0 (2 ^ 32 - 1))\n        |\u003e Random.map (tag Id)\n\n-- the API stays the same even if the implementation changes\ntoBytes --...\ntoString --...\n```\n→ Outside of this module, the only way to create an `Id` is `Id.random`\n\nAgain, above example is just for illustration!\nIn practice, [prefer a narrow type](#always-prefer-narrow-type-over-checked)\nas shown in [`elm-bits`](https://dark.elm.dmy.fr/packages/lue-bird/elm-bits/latest/)\n```elm\ntype alias Id =\n    ArraySized (Exactly N128) Bit\n```\n\n## Combined with [`Tagged`](Typed#Tagged) [`Internal`](Typed#Internal)\n\n```elm\n-- module Password exposing (PasswordUnchecked, PasswordGood, toChecked, length, unchecked)\n\nimport Typed exposing (Typed, Tagged, Checked, Internal, tag, internal)\n\ntype alias Password goodOrUnchecked =\n    Typed goodOrUnchecked PasswordTag Internal String\n\ntype PasswordTag\n    = -- don't expose the tag variant\n      Password\n\ntype alias PasswordGood =\n    Password Checked\n\ntype alias PasswordUnchecked =\n    Password Tagged\n\n-- ! annotates the result as `Tagged` ↓\nunchecked : String -\u003e PasswordUnchecked\nunchecked =\n    tag Password\n\ntoChecked : PasswordUnchecked -\u003e Result String PasswordGood\ntoChecked =\n    \\passwordToTest -\u003e\n        let\n            passwordString =\n                passwordToTest |\u003e internal Password\n        in\n        if (passwordString |\u003e String.length) \u003c 10 then\n            Err \"Use at lest 10 letters \u0026 symbols.\"\n\n        else if commonPasswords |\u003e Set.member passwordString then\n            Err \"Choose a less common password.\"\n\n        else\n            passwordToTest |\u003e Typed.toChecked Password |\u003e Ok\n\ncommonPasswords =\n    Set.fromList\n        [ \"password1234\", \"secret1234\"\n        , \"c001_p4ssw0rd\", \"1234567890\"\n        --...\n        ]\n```\nYou can then decide that only a part of the information should be accessible.\n```elm\n-- doesn't expose too much information\nlength : Password goodOrUnchecked_ -\u003e Int\nlength =\n    \\password -\u003e\n        password\n            |\u003e internal Password\n            |\u003e String.length\n```\nused in\n\n```elm\n-- module Register exposing (State, Event, ui, reactTo, stateInitial)\n\nimport Password exposing (PasswordUnchecked)\n\ntype alias State =\n    { -- accessing user-typed password is impossible\n      passwordTyped : PasswordUnchecked\n    , loggedIn : LoggedIn\n    }\n\nstateInitial : State\nstateInitial =\n    { passwordTyped =\n        \"\" |\u003e Password.unchecked\n    , loggedIn = NotLoggedIn\n    }\n\ntype LoggedIn\n    = -- no user can have an unchecked password\n      LoggedIn { userPassword : PasswordGood }\n    | NotLoggedIn\n\n\ntype Event\n    = PasswordEdited PasswordUnchecked\n    | PasswordConfirmed PasswordGood\n\nreactTo : Event -\u003e (Model -\u003e Model)\nreactTo event =\n    case event of\n        PasswordEdited uncheckedPassword -\u003e\n            \\model -\u003e\n                { model\n                    | passwordTyped = uncheckedPassword\n                }\n        \n        PasswordConfirmed passwordGood -\u003e\n            \\model -\u003e\n                { model\n                    | passwordTyped =\n                        \"\" |\u003e Password.unchecked\n                    , loggedIn =\n                        LoggedIn { userPassword = passwordGood }\n                }\n\nui =\n    \\{ passwordTyped } -\u003e\n        [ [ \"register\" |\u003e Html.text ] |\u003e Html.div []\n        , Html.input\n            [ onInput\n                (\\text -\u003e\n                    text\n                        |\u003e Password.unchecked\n                        -- not accessible from now on\n                        |\u003e PasswordEdited\n                )\n            , String.repeat\n                (passwordTyped\n                    |\u003e Password.length\n                )\n                \"·\"\n                |\u003e Html.value\n            ]\n            []\n        , case passwordTyped |\u003e Password.toChecked of\n            Ok passwordGood -\u003e\n                Html.button\n                    [ onClick (PasswordConfirmed passwordGood) ]\n                    [ \"Create account\" |\u003e Html.text ]\n                \n            Err message -\u003e\n                message |\u003e Html.text\n        ]\n            |\u003e Html.div []\n```\n```elm\npasswordTyped |\u003e untag |\u003e leak\nuserPassword |\u003e untag |\u003e leak\n```\n→ compile-time error: expected `Public` but found `Internal`\n\n# prior art\n\nThis package wouldn't exist without inspiration:\n\n  - [`Punie/elm-id`](https://package.elm-lang.org/packages/Punie/elm-id/latest/)\n\nespecially\n  - [`joneshf/elm-tagged`](https://package.elm-lang.org/packages/joneshf/elm-tagged/latest/)\n  - [`IzumiSy/elm-typed`](https://package.elm-lang.org/packages/IzumiSy/elm-typed/latest/)\n\n\n# limits\n\n## the type of the [`Public`](Typed#Public) untagged thing is not obvious but used often\n\nIn that case expose more descriptive API and leave the rest as \"safe internals\"!\n\nIf you strictly want to avoid allowing [`untag`](Typed#untag) under all circumstances,\nmake it [`Internal`](Typed#Internal)\n\n```elm\ntoDescriptiveValue : TypedThing -\u003e DescriptiveValue\ntoDescriptiveValue =\n    Typed.internal ThingTag\n```\n\n## always prefer narrow type over [`Checked`](Typed#Checked)\n\nMore often than not,\nthere's already a type with the same promises\neven when created directly by users:\n\nInstead of\n\n```elm\ntype alias StringFilled =\n    Typed Checked StringFilledTag Public String\n\ntype alias PasswordLongEnough =\n    Typed Checked PasswordLongEnoughTag Public String\n```\nmake it safe\n```elm\ntype alias StringFilled =\n    { head : Char, tail : String }\n\ntype alias PasswordLongEnough =\n    ArraySized (Min (Fixed N10)) Char\n```\nHere using [`typesafe-array`](https://package.elm-lang.org/packages/lue-bird/elm-typesafe-array/latest/)\n\nUse those! Extensively. No opaque type or [`Checked`](Typed#Checked) necessary\n\n## packages: unnecessary major version bumps\n\nAll ↓ aren't breaking in practice but result in a major version bump\n\n  - [`Checked`](Typed#Checked) → [`Tagged`](Typed#Tagged)\n  - [`Internal`](Typed#Internal) → [`Public`](Typed#Public)\n  - [`Checked`](Typed#Checked) [`Internal`](Typed#Internal) thing type change\n\nFor many package authors, this is a deal-breaker.\n\nBe explicit and choose a `type` for parts of information that could be added or removed in the future.\n\n## can't be defined recursively\n\n```elm\ntype alias Comment =\n    Typed\n        Tagged\n        CommentTag\n        Public\n        { message : String\n        , responses : List Comment\n        }\n```\nelm:\n\u003e This type alias is recursive, forming an infinite type\n\n[recursive alias hint](https://github.com/elm/compiler/blob/master/hints/recursive-alias.md):\n\u003e Somewhere in that cycle, you need to define an actual type to end the infinite expansion.\n\nIn this instance: try tree structures like [`zwilias/elm-rosetree`](https://package.elm-lang.org/packages/zwilias/elm-rosetree/latest/Tree):\n\n```elm\ntype alias Comments =\n    Maybe (Tree { message : String })\n```\n\nFrom the outside, recursive aliases seem like a problem solvable at the language level.\nLet's watch how elm handles them in the future.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flue-bird%2Felm-typed-value","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flue-bird%2Felm-typed-value","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flue-bird%2Felm-typed-value/lists"}