{"id":37459460,"url":"https://github.com/agiledigital/readonly-types","last_synced_at":"2026-01-16T07:02:02.751Z","repository":{"id":37825863,"uuid":"208191613","full_name":"agiledigital/readonly-types","owner":"agiledigital","description":"A collection of readonly TypeScript types inspired by the built-in ReadonlyArray, ReadonlyMap, etc.","archived":false,"fork":false,"pushed_at":"2026-01-02T21:33:07.000Z","size":1875,"stargazers_count":17,"open_issues_count":25,"forks_count":2,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-01-05T00:34:52.876Z","etag":null,"topics":["functional-programming","immutability","typescript"],"latest_commit_sha":null,"homepage":"","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/agiledigital.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2019-09-13T03:53:41.000Z","updated_at":"2025-11-15T18:50:55.000Z","dependencies_parsed_at":"2023-02-19T05:46:02.920Z","dependency_job_id":"e1bd2105-d559-4ba8-8bb9-083281d4accf","html_url":"https://github.com/agiledigital/readonly-types","commit_stats":{"total_commits":352,"total_committers":7,"mean_commits":"50.285714285714285","dds":0.5284090909090908,"last_synced_commit":"752c84978b70c21d65bf680531b42aa2cd0e17db"},"previous_names":["danielnixon/readonly-types"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/agiledigital/readonly-types","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agiledigital%2Freadonly-types","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agiledigital%2Freadonly-types/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agiledigital%2Freadonly-types/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agiledigital%2Freadonly-types/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/agiledigital","download_url":"https://codeload.github.com/agiledigital/readonly-types/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agiledigital%2Freadonly-types/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28477996,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-16T06:30:42.265Z","status":"ssl_error","status_checked_at":"2026-01-16T06:30:16.248Z","response_time":107,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":["functional-programming","immutability","typescript"],"created_at":"2026-01-16T07:02:02.628Z","updated_at":"2026-01-16T07:02:02.727Z","avatar_url":"https://github.com/agiledigital.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Readonly TypeScript Types\n\n[![Build Status](https://github.com/agiledigital/readonly-types/actions/workflows/main.yml/badge.svg)](https://github.com/agiledigital/readonly-types/actions/workflows/main.yml)\n[![type-coverage](https://img.shields.io/badge/dynamic/json.svg?label=type-coverage\u0026prefix=%E2%89%A5\u0026suffix=%\u0026query=$.typeCoverage.atLeast\u0026uri=https%3A%2F%2Fraw.githubusercontent.com%2Fagiledigital%2Freadonly-types%2Fmaster%2Fpackage.json)](https://github.com/plantain-00/type-coverage)\n[![codecov](https://codecov.io/gh/agiledigital/readonly-types/branch/master/graph/badge.svg?token=SYO6NY3DF0)](https://codecov.io/gh/agiledigital/readonly-types)\n[![Mutation testing badge](https://img.shields.io/endpoint?style=flat\u0026url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fagiledigital%2Freadonly-types%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/agiledigital/readonly-types/master)\n[![Known Vulnerabilities](https://snyk.io/test/github/agiledigital/readonly-types/badge.svg?targetFile=package.json)](https://snyk.io/test/github/agiledigital/readonly-types?targetFile=package.json)\n[![npm](https://img.shields.io/npm/v/readonly-types.svg)](https://www.npmjs.com/package/readonly-types)\n\nA collection of readonly TypeScript types inspired by TypeScript's built-in readonly types (`ReadonlyArray`, `ReadonlyMap`, etc) and by [is-immutable-type](https://github.com/RebeccaStevens/is-immutable-type).\n\nThe types here are all fully `Immutable` following [is-immutable-type#definitions](https://github.com/RebeccaStevens/is-immutable-type#definitions).\n\nThis package assumes you have TypeScript's [strict mode](https://www.typescriptlang.org/tsconfig#strict) and [noUncheckedIndexedAccess](https://www.typescriptlang.org/tsconfig#noUncheckedIndexedAccess) option turned on. [eslint-plugin-total-functions](https://github.com/danielnixon/eslint-plugin-total-functions/) provides an ESLint rule to ensure they're both on.\n\n## Installation\n\n```sh\n# yarn\nyarn add readonly-types\n\n# npm\nnpm install readonly-types\n```\n\n## Usage\n\n```TypeScript\n// Here's an example using ReadonlyURL.\nimport { ReadonlyURL } from \"readonly-types\";\n\n// This is fine.\nconst hasFooSearchParam = (url: ReadonlyURL) =\u003e url.searchParams.has(\"foo\");\n\n// But this won't compile.\nconst setFooSearchParam = (url: ReadonlyURL) =\u003e url.searchParams.set(\"foo\", \"bar\");\n```\n\n## The Types\n\nThe second column contains the types provided by this library (which are all `Immutable`). The columns to the right of it show the types being replaced and what level of immutability they achieve by default.\n\nThe first column (\"Even Better 🚀\") contains types that are more than just immutable versions of the types in the later columns. These \"even better\" options require more effort to adopt than those in the second column (or may not even be generally available yet), but they're worth considering if you want something that is more closely aligned with a pure typeful functional programming approach.\n\n| Even Better 🚀 | Immutable | ReadonlyDeep | ReadonlyShallow | Mutable |\n|----------------|-----------|--------------|-----------------|---------|\n| A dedicated `Map` type (good options below), see [Objects vs. Maps](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map#objects_vs._maps) for why | `ReadonlyRecord` | | | [`Record`](https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkt) |\n| | `ReadonlyURL` | | | [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) | \n| | `ReadonlyURLSearchParams` | | | [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) |\n| [Temporal](https://tc39.es/proposal-temporal/) (stage 3 proposal, aims to solve various problems in `Date`, including its mutability) | `ReadonlyDate` | | | [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) |\n| [Chunk](https://effect.website/docs/data-types/chunk/), [PrincipledArray](https://github.com/agiledigital/readonly-types/issues/7) (does not return mutable arrays from methods like `map`), purpose-built immutable data structures | `ImmutableArray` | `ReadonlyArray` | | `Array` |\n| purpose-built immutable data structures | `ImmutableSet` | `ReadonlySet` | | `Set` |\n| purpose-built immutable data structures | `ImmutableMap` | `ReadonlyMap` | | `Map` |\n| | `ReadonlyWeakSet` | | | [`WeakSet`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet) |\n| | `ReadonlyWeakMap` | | | [`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) |\n| [Effect's `Either`](https://effect.website/docs/data-types/either/), [fp-ts's `Either`](https://gcanti.github.io/fp-ts/modules/Either.ts.html) | `ReadonlyError` (and friends) | | | [`Error` and friends](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects#error_objects) |\n| | `ReadonlyRegExp` | | | `RegExp` |\n| [Effect](https://effect.website/docs/getting-started/the-effect-type/), [fp-ts's `TaskEither`](https://gcanti.github.io/fp-ts/modules/TaskEither.ts.html) | `ReadonlyPromise` | `Promise` | | |\n| | `DeepImmutable` | | | [`DeepReadonly` from ts-essentials](https://github.com/ts-essentials/ts-essentials/blob/master/lib/types.ts#L156-L181), which when used will produce a mix of `Mutable` and `ReadonlyDeep` types |\n* PRs welcome!\n\n## Linting\n\nYou can ban the mutable counterparts to these readonly types using [eslint-plugin-functional](https://github.com/eslint-functional/eslint-plugin-functional/)'s [prefer-immutable-types](https://github.com/eslint-functional/eslint-plugin-functional/blob/main/docs/rules/prefer-immutable-types.md) rule.\n\n## `ImmutableArray` and `PrincipledArray`\n\nTypeScript's built-in `ReadonlyArray` isn't truly immutable. Observe:\n\n```typescript\nconst foo: ReadonlyArray\u003cstring\u003e = [\"\"] as const;\n\n// This compiles\nfoo.every = () =\u003e false;\n// So does this\nfoo.at = () =\u003e undefined;\n```\n\nis-immutable-type provides the answer in [Making ReadonlyDeep types Immutable](https://github.com/RebeccaStevens/is-immutable-type#making-readonlydeep-types-immutable). We've reused that here to provide an `ImmutableArray` type.\n\n```typescript\nimport { ImmutableArray } from \"readonly-types\";\n\nconst foo: ImmutableArray\u003cstring\u003e = [\"\"] as const;\n\n// These no longer compile\nfoo.every = () =\u003e false; // Cannot assign to 'every' because it is a read-only property. ts(2540)\nfoo.at = () =\u003e undefined; // Cannot assign to 'at' because it is a read-only property. ts(2540)\n```\n\n`ReadonlyArray` achieves the `ReadonlyDeep` level of immutability, `ImmutableArray` achieves the `Immutable` level.\n\nIt turns out that even `ImmutableArray` has cracks in its immutable armour. Here's a subtle one:\n\n```typescript\n// This doesn't compile...\nfoo.at = () =\u003e undefined;\n\nfoo.map((value, index, array) =\u003e {\n  // ... but this does!\n  array.at = () =\u003e undefined;\n\n  return value;\n});\n```\n\nThe `array` passed as the third argument to the `map` callback is typed as `ReadonlyArray`. Our `ImmutableArray` trick doesn't change that method's callback's argument's types. The same applies to `filter`, `flatMap`, `find` and so on.\n\nTo fix that issue we provide a type called `PrincipledArray`:\n\n```typescript\nconst foo: PrincipledArray\u003cstring\u003e = [\"\"] as const;\n\n// This doesn't compile...\nfoo.at = () =\u003e undefined;\n\nfoo.map((value, index, array) =\u003e {\n  // ... and neither does this!\n  array.at = () =\u003e undefined;\n\n  return value;\n});\n```\n\n`PrincipledArray` makes a few other (type-incompatible) improvements while its at it, including:\n\n* Removes `forEach` entirely (use `map` or another non-side-effecting alternative instead).\n* Requires a true boolean return type from predicates passed to `filter` and other methods (by default, TypeScript allows these predicates to return `unknown`).\n* Removes the partial versions of `reduce` and `reduceRight` that throw at runtime if the array is empty (i.e. those that don't require the caller to specify an initial value). See also https://github.com/eslint-functional/eslint-plugin-functional/issues/527\n\n```typescript\nimport { principledArray } from \"readonly-types\";\n\n// Given a principled array.\nconst foo = principledArray\u003cstring\u003e([]);\n\n// This does not compile.\n// Property 'forEach' does not exist on type 'PrincipledArray\u003cstring\u003e'. ts(2339)\nfoo.forEach(() =\u003e {});\n\n// This would normally throw at runtime, but with PrincipledArray it does not compile\n// Expected 2 arguments, but got 1. ts(2554)\n// An argument for 'initialValue' was not provided.\nconst result = foo.reduce((p) =\u003e p);\n```\n\nThe downside to `PrincipledArray` is that -- precisely because it changes the type in these ways -- you cannot assign it to a value of type `ReadonlyArray`. `ImmutableArray` doesn't have this downside. Choose whichever is most appropriate for you.\n\n## `ImmutableNonEmptyArray` and `PrincipledNonEmptyArray`\n\nAn array type that is verifiably non-empty (i.e. known to have at least one entry at compile time) is a useful type to have.\n\nYou can make such a type based on `ReadonlyArray` like this:\n\n```typescript\ntype ReadonlyNonEmptyArray\u003cT\u003e = readonly [T, ...(readonly T[])];\n```\n\nLike `ReadonlyArray` that type is only `ReadonlyDeep`, not truly `Immutable`.\n\nWe provide a truly immutable version in the form of `ImmutableNonEmptyArray`.\n\nWith `PrincipledArray` having removed the versions of `reduce` and `reduceRight` that do not require an `initialValue`, there becomes a need for another type that is verifiably non-empty (at compile time) which puts them back again.\n\nWe provide that type in the form of `PrincipledNonEmptyArray`, which you can think of as a mix between `ImmutableNonEmptyArray` and `PrincipledArray`:\n\n```typescript\n// Given a principled non-empty array.\nconst foo = principledNonEmptyArray\u003cstring\u003e([\"a\"]);\n\n// This compiles, whereas it wouldn't have compiled for a regular principled array.\nconst result = foo.reduce((p) =\u003e p);\n```\n\n## Array type compatibility\n\n| ⬇️ can be assigned to ➡️ | `Array` | `ReadonlyArray`   | `ImmutableArray`  | `PrincipledArray` | `PrincipledNonEmptyArray` |\n|---------------------------|--------|-------------------|-------------------|-------------------|---------------------------|\n| `Array`                   | ✅     | ✅ ⚠️             | ✅ ⚠️             | ❌                | ❌ |\n| `ReadonlyArray`           | ❌     | ✅                | ✅ ⚠️             | ❌                | ❌ |\n| `ImmutableArray`          | ❌     | ✅ ⚠️             | ✅                | ❌                | ❌ |\n| `PrincipledArray`         | ❌     | ❌                | ❌                | ✅                | ❌ |\n| `PrincipledNonEmptyArray` | ❌     | ❌                | ❌                | ✅                | ✅ |\n\nAssignments marked ⚠️ can lead to surprising mutation in whichever side of the assignment appears to have \"more\" immutability, via mutations made to the side that has \"less\". [eslint-plugin-total-functions](https://github.com/danielnixon/eslint-plugin-total-functions/) includes an ESLint rule to flag these unsafe assignments. https://github.com/eslint-functional/eslint-plugin-functional/issues/526 may play a part too. See https://github.com/Microsoft/TypeScript/issues/13347 for more.\n\n## Purpose-built immutable data structures\n\nTypes like `ImmutableArray` and `PrincipledArray` (and even the humble built-in `ReadonlyArray`) can help a lot with correctness but the underlying runtime type remains a mutable `Array`. The same goes for our immutable `Set` and `Map` types. In essence the data structures are the same, we're just constraining ourselves to an immutable subset of their mutable APIs.\n\nOne consequence of this is that if someone could get their hands on a mutable handle to one of our values, they could edit it as if it were mutable (e.g. via an `as` type assertion or via an `Array.isArray` check). This forces us to put a little asterisk next to any immutability guarantees we make. You might reach for [Object.freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze) in response to that risk, but that comes with its own issues (performance, compatibility, doesn't show up in the type system, ...).\n\nAnother consequence of this is that updating and copying values of these types is needlessly expensive (in terms of compute and memory). A copy of the _entire_ structure must be taken to preserve correctness, even if all we want to do for example is update a single element.\n\nThere exist purpose-built immutable data structures that give us an immutable API without the associated performance cost of copying an underlying mutable structure (look for terms like 'structural sharing' and 'copy on write'). If performance is a factor for you, these can be a better choice than the immutable types provided by this package.\n\nTo get you started, check out the following:\n\n* https://github.com/immerjs/immer\n* https://github.com/immutable-js/immutable-js\n* https://github.com/rtfeldman/seamless-immutable\n\nA surprising irony of these types is that they typically aren't truly immutable, for the same reason that `ReadonlyArray` isn't truly immutable. Here's an example:\n\n```typescript\nimport { Map as ImmutableJsMap } from \"immutable\";\nconst foo = ImmutableJsMap([[\"key\", \"value\"]]);\n// This compiles\nfoo.delete = () =\u003e foo;\n```\n\nBecause `delete` is implemented using method syntax it is necessarily mutable (TypeScript methods defined using method syntax cannot be readonly for \"reasons\"). This is so common that [is-immutable-type#definitions](https://github.com/RebeccaStevens/is-immutable-type#definitions) defines a level of \"readonly-ness\" called `ReadonlyDeep` that sits below truly `Immutable` but above the mutable levels `ReadonlyShallow` and `Mutable`.\n\nDepending on how strictly you wish to enforce immutability, `ReadonlyDeep` may or may not be acceptable to you. If it isn't, you can fix it like this:\n\n```typescript\nimport { Map as ImmutableJsMap } from \"immutable\";\n\ntype TrulyImmutableMap\u003cK, V\u003e = Readonly\u003cImmutableJsMap\u003cK, V\u003e\u003e;\n\nconst foo: TrulyImmutableMap\u003cstring, string\u003e = ImmutableJsMap([\n  [\"key\", \"value\"],\n]);\n\n// No longer compiles\nfoo.delete = () =\u003e foo; // Cannot assign to 'delete' because it is a read-only property. ts(2540)\n```\n\nSee [Making ReadonlyDeep types Immutable](https://github.com/RebeccaStevens/is-immutable-type#making-readonlydeep-types-immutable) for more on this.\n\n## See Also\n* https://github.com/danielnixon/eslint-config-typed-fp\n* https://github.com/jonaskello/eslint-plugin-functional\n* https://github.com/danielnixon/eslint-plugin-total-functions\n* https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html#readonly-and-const\n* To see ReadonlyDate adoption grow, upvote this: https://github.com/date-fns/date-fns/issues/1944\n* https://github.com/Microsoft/TypeScript/issues/13347\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fagiledigital%2Freadonly-types","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fagiledigital%2Freadonly-types","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fagiledigital%2Freadonly-types/lists"}