{"id":18663215,"url":"https://github.com/pskfyi/handy","last_synced_at":"2026-04-27T11:31:23.822Z","repository":{"id":154521166,"uuid":"627667171","full_name":"pskfyi/handy","owner":"pskfyi","description":"Assorted utility functions, classes, and types for Deno.","archived":false,"fork":false,"pushed_at":"2025-07-27T02:17:52.000Z","size":303,"stargazers_count":0,"open_issues_count":38,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-07-27T02:25:52.729Z","etag":null,"topics":["deno"],"latest_commit_sha":null,"homepage":"https://deno.land/x/handy","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"0bsd","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pskfyi.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}},"created_at":"2023-04-14T00:22:34.000Z","updated_at":"2025-07-27T01:38:28.000Z","dependencies_parsed_at":null,"dependency_job_id":"39cdfe03-2aca-418d-9985-e76a4c283282","html_url":"https://github.com/pskfyi/handy","commit_stats":null,"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/pskfyi/handy","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pskfyi%2Fhandy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pskfyi%2Fhandy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pskfyi%2Fhandy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pskfyi%2Fhandy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pskfyi","download_url":"https://codeload.github.com/pskfyi/handy/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pskfyi%2Fhandy/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32335295,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-26T23:26:28.701Z","status":"online","status_checked_at":"2026-04-27T02:00:06.769Z","response_time":128,"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":["deno"],"created_at":"2024-11-07T08:15:41.978Z","updated_at":"2026-04-27T11:31:23.810Z","avatar_url":"https://github.com/pskfyi.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 👋 Handy\n\n[![JSR](https://jsr.io/badges/@psk/handy)](https://jsr.io/@psk/handy) [![deno module](https://shield.deno.dev/x/handy)](https://deno.land/x/handy)\n\nUtility functions, classes, types, and scripts in uncompiled TS, for Deno.\n\n- [`array`](#array)\n- [`cli`](#cli)\n- [`collection`](#collection)\n- [`deno`](#deno)\n- [`env`](#env)\n- [`fs`](#fs)\n- [`git`](#git)\n- [`graph`](#graph)\n- [`io`](#io)\n- [`js`](#js)\n- [`json`](#json)\n  - [`Json` namespace](#json-namespace)\n  - [`JsonMergePatch` namespace](#jsonmergepatch-namespace)\n  - [`JsonPatch` namespace](#jsonpatch-namespace)\n  - [`JsonPointer` namespace](#jsonpointer-namespace)\n  - [`JsonTree` namespace](#jsontree-namespace)\n- [`md`](#md)\n- [`mermaid`](#mermaid)\n- [`number`](#number)\n- [`object`](#object)\n- [`os`](#os)\n- [`parser`](#parser)\n- [`path`](#path)\n- [`scripts`](#scripts)\n  - [`makeReleaseNotes`](#makereleasenotes)\n  - [`updateExports`](#updateexports)\n- [`string`](#string)\n- [`ts`](#ts)\n\n## `array`\n\nArray-related utilities.\n\n```ts\nimport { mapOnInterval, Tuple, TypedArray } from \"jsr:@psk/handy/array\";\n\nconst say = (item: unknown) =\u003e {/* No-op for demonstration */};\n\nawait mapOnInterval([3, 2, 1, \"go!\"], 1, say);\n// say: 3\n// 100ms later, say: 2\n// 100ms later, say: 1\n// 100ms later, say: \"go!\"\n\nconst arr: TypedArray = new Uint8Array();\n\ntype Filled = Tuple.Fill\u003c[\"a\", \"b\"], 7\u003e; // [7, 7]\ntype Flattened = Tuple.Flat\u003c[[1, 2], [3, 4]]\u003e; // [1, 2, 3, 4]\ntype Indices = Tuple.Indices\u003c[\"a\", \"b\", \"c\"]\u003e; // [0, 1, 2]\ntype Index = Tuple.Index\u003c[\"a\", \"b\", \"c\"]\u003e; // 0 | 1 | 2\ntype Reversed = Tuple.Reverse\u003c[1, 2, 3]\u003e; // [3, 2, 1]\ntype Deed = Tuple.FromIndices\u003c[\"d\", \"e\"], [0, 1, 1, 0]\u003e; // [\"d\", \"e\", \"e\", \"d\"]\ntype ThreeUnknowns = Tuple.OfLength\u003c3\u003e; // [unknown, unknown, unknown]\n```\n\n## `cli`\n\nCLI-related utilities.\n\n```ts\nimport { cmd, cmds, consoleWidth } from \"jsr:@psk/handy/cli\";\n\nawait cmd(\"deno -V\"); // ex: \"deno 1.34.0\"\nawait cmds([\"deno -V\", \"deno -h\"]); // executes all and provides a summary of successes and failures\nconsoleWidth(80); // real width of terminal, or fallback of 80\n```\n\n## `collection`\n\nUtilities related to generic collection types, like `Iterable`s.\n\n```ts\nimport { largest, position, smallest } from \"jsr:@psk/handy/collection\";\n\nlargest([\"aaa\", \"b\", \"cc\"]); // \"aaa\"\nsmallest([\"aaa\", \"b\", \"cc\"]); // \"b\"\n\nlargest(\"size\", [new Set([1]), new Set([2, 3]), new Set()]); // new Set([2, 3])\nsmallest(\"size\", [new Set([1]), new Set([2, 3]), new Set()]); // new Set()\n\n// a Position is a location between items in a collection\nposition.toPosition(-1, [\"a\", \"b\", \"c\"]); // 2, position between b and c\nposition.toPosition(-1, \"abc\"); // 2, position between b and c\n\n// -0 is the end of the collection\nposition.toPosition(-0, [\"a\", \"b\", \"c\"]); // 3\nposition.toPosition(-0, \"abc\"); // 3\n\nposition.next(0, \"a\"); // 1, after a\nposition.next(1, \"a\"); // null, no next position exists\nposition.previous(1); // 0 (no collection needed)\nposition.previous(0); // null, no previous position exists\n\nposition.isPosition(NaN, []); // false\nposition.assert(0, []); // 0 is always valid\n```\n\n```ts\nimport { Index, IndexedCollection, Indices } from \"jsr:@psk/handy/collection\";\n\nconst arr = [\"a\", \"b\", \"c\"] satisfies IndexedCollection;\nconst str = \"XY\" satisfies IndexedCollection;\nconst typedArr = new Uint8Array() satisfies IndexedCollection;\n\ntype ArrIndices = Indices\u003ctypeof arr\u003e; // [0, 1, 2]\ntype StrIndices = Indices\u003ctypeof str\u003e; // [0, 1]\ntype TypedArrIndices = Indices\u003ctypeof typedArr\u003e; // number[]\n\ntype ArrIndex = Index\u003ctypeof arr\u003e; // 0 | 1 | 2\ntype StrIndex = Index\u003ctypeof str\u003e; // 0 | 1\ntype TypedArrIndex = Index\u003ctypeof typedArr\u003e; // number\n```\n\n## `deno`\n\nDeno exports utilities.\n\n```ts\nimport { coverage } from \"jsr:@psk/handy/deno/coverage\";\nimport { determine } from \"jsr:@psk/handy/deno/exports/determine\";\n\nawait determine(\"./_test/fixture/deno\", {/* Options */});\n// { \".\": \"./mod.ts\", \"some/path\": \"some/path.ts\" }\n\nawait coverage().catch(() =\u003e {});\n```\n\n## `env`\n\nEnvironment variable utilities.\n\n```ts\nimport * as env from \"jsr:@psk/handy/env\";\n\nenv.boolean(\"MY_VAR\"); // handles \"false\", \"0\", \"\", and undefined\nenv.number(\"MY_VAR\"); // returns env var as a number or null\nenv.string(\"MY_VAR\"); // returns env var as a string\n```\n\n## `fs`\n\nFile system-related utilities.\n\n```ts\nimport { findNearestFile, glob, globImport } from \"jsr:@psk/handy/fs\";\n\nawait findNearestFile(\".\", \"some.file\"); // \"../../some.file\"\nawait glob(\"./**/*.ts\"); // all TS files in cwd and subdirs\n\nconst modules = await globImport(\"./**/*.ts\");\n\nfor (const [path, module] of Object.entries(modules)) {\n  console.log(path); // something.ts\n  const data = await module(); // import something.ts\n}\n```\n\nShortcuts for file system operations:\n\n```ts\nimport {\n  readJsonFile,\n  replaceJsonFile,\n  replaceTextFile,\n  writeJsonFile,\n} from \"jsr:@psk/handy/fs\";\n// sync variants are also available\n```\n\n## `git`\n\nGit-related utilities.\n\n```ts\nimport { assertUnmodified, commit, tag } from \"jsr:@psk/handy/git\";\n\nawait tag.getLatest().catch(); // ex. \"v1.0.0\"\n\nawait commit.sha(\"HEAD\").catch(() =\u003e {}); // ex. \"a1b2c3d4e5f6...\"\nawait commit.get(\"HEAD\").catch(() =\u003e {}); // { message: \"...\", ... }\ncommit.conventional.parse(\"feat(scope)!: description\"); // { type: \"feat\", ... }\n\nawait assertUnmodified().catch(() =\u003e {/* unstaged changes detected */});\nawait assertUnmodified(\"deno.json\").catch(() =\u003e {/* can target files */});\n```\n\n## `graph`\n\nGraph-related utilities.\n\n```ts\nimport { DirectedGraph } from \"jsr:@psk/handy/graph\";\n\nconst graph = new DirectedGraph\u003cstring\u003e()\n  .add(\"a\")\n  .add(\"b\", [\"a\", \"c\"]);\n\ngraph.vertices; // new Set([\"a\", \"b\", \"c\"])\ngraph.edges; // new Set([[\"a\", \"c\"]])\n```\n\n## `io`\n\nAssorted I/O utilities which don't fit in other categories.\n\n```ts\nimport { clipboard } from \"jsr:@psk/handy/io\";\n\n// NOTE: Only supports MacOS and Windows\nawait clipboard.copy(\"foo\").catch(console.log);\nawait clipboard.paste().catch(console.log); // \"foo\"\n```\n\n## `js`\n\nJavaScript utilities.\n\n```ts\nimport { evaluate } from \"jsr:@psk/handy/js\";\n\nconst { stdout } = await evaluate(\"console.log('Hello!')\");\n//         ^? \"Hello!\"\n```\n\n## `json`\n\nJSON-related utilities.\n\n```ts\nimport { PrettyError } from \"jsr:@psk/handy/json\";\n\nnew PrettyError(\"Message\", {/* A JSON object to pretty-print */});\n```\n\n### `Json` namespace\n\nA convenience wrapper around baseline JSON utils and types. The utils are also available under `jsr:@psk/handy/json/utils`, and the types under `jsr:@psk/handy/json/types`.\n\n```ts\nimport { Json } from \"jsr:@psk/handy/json\";\n\nconst a: Json.Primitive = \"some string\"; // or number, boolean, null\nconst b: Json.Array = [1, [\"2\", true], { a: null }];\nconst c: Json.Object = { a: 1, b: [\"2\", true], d: { e: null } };\n// Json.Value = any of the above\n\nconst value = \"example\";\n\ntype T = Json.TypeName; // \"string\" | \"number\" | \"boolean\" | \"null\" | \"object\" | \"array\"\nJson.typeOf(value); // returns a Json.TypeName\nJson.shallowTypeOf(value); // doesn't check array/obj children\n\nJson.clone(value);\nJson.prettyPrint(value);\nJson.minify(value);\nJson.parse(\"{}\");\n\nJson.equals({ a: 1 }, { b: 2 }); // deep equality per JSON Patch spec\n\n// Type guards\nJson.isValue(value);\nJson.isPrimitive(value);\nJson.isArray(value);\nJson.isObject(value);\nJson.isObjectShallow(value); // confirms it's not null, Array, Map, Set, etc.\n```\n\n### `JsonMergePatch` namespace\n\nHomegrown JSON Merge Patch utilities based on [the official spec](https://datatracker.ietf.org/doc/html/rfc7396).\n\n```ts\nimport { assertEquals } from \"@std/assert/equals\";\nimport { JsonMergePatch } from \"jsr:@psk/handy/json\";\n\nJsonMergePatch.MEDIA_TYPE; // \"application/merge-patch+json\"\n\n// Diff to create a patch\nconst before = { A: 7, B: { C: true } };\nconst after = { A: 7, B: { D: [\"Hello\"] } };\n\nassertEquals(\n  JsonMergePatch.diff(before, after),\n  { B: { C: null, D: [\"Hello\"] } },\n);\n\n// Apply patches\nconst target = { A: { B: { C: true } } };\nconst patch = { A: { B: { C: false, D: [\"Hello\"] } } }; // update C, insert D\n\nJsonMergePatch.apply(target, patch); // Mutates target\n```\n\n### `JsonPatch` namespace\n\nJSON Patch implementation based on [the official spec](https://datatracker.ietf.org/doc/html/rfc6902), and related utilities. See the module for more details.\n\n### `JsonPointer` namespace\n\nJSON Pointer implementation, with utilities, based on [the official spec](https://datatracker.ietf.org/doc/html/rfc6901). See the module for more details.\n\n### `JsonTree` namespace\n\nHomegrown utilities for working with JSON as a tree structure. See the module for more details.\n\n## `md`\n\nMarkdown-related utilities.\n\n````ts\nimport { codeBlock, fillCommentBlocks, table } from \"jsr:@psk/handy/md\";\n\ncodeBlock.create(\"grep\"); // \"    grep\"\ncodeBlock.create(\"const a: number = 1\", { lang: \"ts\" });\ncodeBlock.parse(\"```ts\\nconst a: number = 1\\n```\");\ncodeBlock.findAll(\"    grep\\n```cd```\"); // [\"    grep\", \"```cd```\"]\ncodeBlock.evaluate(\n  codeBlock.create('console.log(\"Hello!\")', { lang: \"ts\" }),\n);\n\nfillCommentBlocks(\n  `\u003c!-- start my-block --\u003e\n\u003c!-- end my-block --\u003e`,\n  { \"my-block\": \"Content to fill the block\" },\n);\n\nconst [result, cursor] = table.parser.parse(\n  `| Header 1 | Header 2 |\n| -------- | -------- |\n| Row 1    | Row 2    |\n| Row 3    | Row 4    |`,\n);\n````\n\n## `mermaid`\n\n```ts\nimport { flowchart } from \"jsr:@psk/handy/mermaid\";\nimport { DirectedGraph } from \"jsr:@psk/handy/graph\";\nimport { codeBlock } from \"jsr:@psk/handy/md\";\n\nconst graph = new DirectedGraph\u003cstring\u003e();\n\ngraph.add([\"a\", \"b\", \"c\", \"d\"], [\"h\", \"i\", \"j\", \"k\"], [\"c\", \"h\"], [\"d\", \"i\"]);\n\nflowchart(graph.edges, { title: \"Example Flowchart\" }); //outputs mermaid flowchart markdown below\n```\n\n```mermaid\n---\ntitle: Example Flowchart\n---\nflowchart LR\n    a --\u003e b\n    b --\u003e c\n    c --\u003e d\n    c --\u003e h\n    d --\u003e i\n    h --\u003e i\n    i --\u003e j\n    j --\u003e k\n```\n\n## `number`\n\nNumber-related utilities.\n\n```ts\nimport { Num } from \"jsr:@psk/handy/number\";\n\ntype T = Num.Type\u003c1.1\u003e; // \"+float\"\ntype U = Num.Type\u003c0\u003e; // \"zero\"\ntype V = Num.Type\u003c-5\u003e; // \"-integer\"\n\n// type filters return `never` if the type doesn't match\ntype Finite = Num.Finite\u003c0\u003e; // 0\ntype NotFinite = Num.Finite\u003cnumber\u003e; // never\n\ntype Wide = Num.Wide\u003cnumber\u003e; // number\ntype NotWide = Num.Wide\u003c1\u003e; // never\n\ntype Int = Num.Integer\u003c1\u003e; // 1\ntype NotInt = Num.Integer\u003c1.1\u003e; // never\n\ntype Float = Num.Float\u003c1.1\u003e; // 1.1\ntype NotFloat = Num.Float\u003c1\u003e; // never\n```\n\n## `object`\n\nObject-related utilities.\n\n```ts\nimport { setNestedEntry } from \"jsr:@psk/handy/object\";\n\nconst S = Symbol(\"symbol\");\nsetNestedEntry({}, [\"a\", 10, S], \"👋\"); // { a: { 10: { [S]: \"👋\" } } }\n```\n\n```ts\nimport type { Obj } from \"jsr:@psk/handy/object\";\n\ntype Key = Obj.Key; // string | number | symbol\ntype Empty = Obj.Entry; // Record\u003cKey, never\u003e\ntype Entry = Obj.Entry\u003cany\u003e; // [Key, any]\ntype Pair = Obj.Pair\u003c\"a\", number\u003e; // { \"a\": number }\ntype EntryToPair = Obj.EntryToPair\u003cEntry\u003e; // Pair\ntype MyObj = Obj.FromEntries\u003c[[\"a\", 1], [\"b\", null]]\u003e; // { a: 1, b: null }\ntype Entries = Obj.ToEntries\u003cMyObj\u003e; // Array\u003c[\"a\", 1], [\"b\", null]\u003e\n```\n\n## `os`\n\nOS-related utilities.\n\n```ts\nimport { posixNewlines } from \"jsr:@psk/handy/os\";\n\nposixNewlines(\"A\\r\\nB\\rC\"); // \"A\\nB\\nC\"\n```\n\n## `parser`\n\nA parser combinator library.\n\n```ts\nimport { sequence, string } from \"jsr:@psk/handy/parser\";\n\nconst dash = string(\"-\").ignore;\nconst phoneNumber = sequence(/\\d{3}/, dash, /\\d{3}/, dash, /\\d{4}/);\n\nconst [result] = phoneNumber.parse(\"123-456-7890\");\n//       ^? [\"123\", \"456\", \"7890\"]\n```\n\n## `path`\n\nPath-related utilities.\n\n```ts\nimport { globRoot } from \"jsr:@psk/handy/path\";\n\nglobRoot(\"a/b/**/*.ts\"); // \"a/b/\"\n```\n\n## `scripts`\n\nEach script supports CLI usage including a `--help` / `-h` flag, or a programmatic API.\n\n### `makeReleaseNotes`\n\n\u003c!-- start make-release-notes --\u003e\n\n```\nIn a git repo, scan the commit history for conventional commits since the last tag and generate a markdown-formatted list of features and fixes.\n\nUsage:\n  deno run -A jsr:@psk/handy/scripts/makeReleaseNotes [options] [path]\n\nArguments:\n  path    Path to a git repo to scan. Defaults to the current working directory.\n\nOptions:\n  -h, --help          Show this help message\n  -c, --to-clipboard  Copy release notes to clipboard\n  -i, --inclusive     Include the first commit\n  -v, --verbose       Print verbose output\n  -g, --group-by-type Group commits by type using H2 headings\n  --commit=\u003ccommit\u003e   Commit to use as base for release notes\n  --types=\u003ctypes\u003e     Comma-separated list of types to include\n  --\u003ctype\u003e=\u003cname\u003e     Name to use for a type's H2 when grouping by type\n\nExamples:\n  deno run -A jsr:@psk/handy/scripts/makeReleaseNotes -cgv\n\n  deno run -A jsr:@psk/handy/scripts/makeReleaseNotes --commit v1.0.0\n\n  deno run -A jsr:@psk/handy/scripts/makeReleaseNotes \\\\\n    --types=feat,custom --custom=\"Custom's Section Heading\"\n```\n\n\u003c!-- end make-release-notes --\u003e\n\n### `updateExports`\n\n\u003c!-- start update-exports --\u003e\n\n```\nUpdates the exports field in a deno.json file to include .ts files in the current directory and its subdirectories, sorted by key. Excludes files and directories that start with a dot or underscore, and test files.\n\nUsage:\n  deno run -A jsr:@psk/handy/scripts/updateExports [path]\n\nArguments:\n  path    A deno.json file or directory containing one. Searches the current directory by default.\n\nOptions:\n  -h, --help         Show this help message\n  -d, --dry-run      Show what would be done without making any changes\n  -a, --assert       Returns exit code 1 if the file has unstaged changes after running the script. Defaults to true in CI and false otherwise.\n  -r, --root=\u003cpath\u003e  Make export paths relative to the provided path. Defaults to the deno.json file's directory.\n\nExamples:\n  deno run -A jsr:@psk/handy/scripts/updateExports\n\n  deno run -A jsr:@psk/handy/scripts/updateExports ./path/to/deno.json\n\n  deno run -A jsr:@psk/handy/scripts/updateExports --root=src\n```\n\n\u003c!-- end update-exports --\u003e\n\n## `string`\n\nString-related utilities.\n\n```ts\nimport {\n  dedent,\n  elideStart, // and others\n  escapeTerse,\n  indent,\n  mostConsecutive,\n  replaceMany,\n  sequences,\n  splitOn,\n  splitOnFirst,\n  Text,\n  TextCursor,\n} from \"jsr:@psk/handy/string\";\n\ndedent(\"  a\\n   b\\n    c\"); // \"a\\n b\\n  c\"\nindent(\"a\\nb\\nc\", 2); // \"  a\\n  b\\n  c\"\nelideStart(\"1234567890\", { maxLength: 8 }); // \"…4567890\"\nescapeTerse(\"\\t\\t\\n\"); // \"⇥⇥¶\"\nreplaceMany(\"aabbcc\", { a: \"X\", b: \"Y\" }); // \"XXYYcc\"\nsplitOnFirst(\"/\", \"a/b/c\"); // [\"a\", \"b/c\"]\nsplitOn(3, \"\\n\", \"a\\nb\\nc\\nd\\ne\"); // [\"a\", \"b\", \"c\", \"d\\ne\"]\nsequences(\"A\", \"ABAACA\"); // [\"A\", \"AA\", \"A\"]\nmostConsecutive(\"A\", \"ABAACA\"); // 2\n\nconst text = new Text(\"a\\nb\\nc\");\ntext.lines; // [\"a\\n\", \"b\\n\", \"c\"]\ntext.locationAt(4); // location of \"c\", { line: 3, column: 1, offset: 4 }\ntext.locationAt(5); // end of text, { line: 3, column: 2, offset: 5 }\n\nconst cursor = new TextCursor(\"a\\nb\\nc\", 2);\ncursor.remainder; // \"b\\nc\"\ncursor.location; // { offset: 2, line: 2, column: 1 }\ncursor.inspect(); // string depicting...\n// [L2] b¶\n//       ^\n```\n\n```ts\nimport { Str } from \"jsr:@psk/handy/string/types\";\n\ntype Char = Str.Char\u003c\"ABC\"\u003e; // \"A\" | \"B\" | \"C\"\ntype Index = Str.Index\u003c\"ABC\"\u003e; // 0 | 1 | 2\ntype Indices = Str.Indices\u003c\"ABC\"\u003e; // [0, 1, 2]\ntype Tuple = Str.ToTuple\u003c\"ABC\"\u003e; // [\"A\", \"B\", \"C\"]\n```\n\n## `ts`\n\nTypeScript-related utilities.\n\n```ts\nimport { evaluate } from \"jsr:@psk/handy/ts\";\n\nawait evaluate(\"console.log('Hello!')\")\n  .then((res) =\u003e res.stdout); // \"Hello!\"\n```\n\n```ts\nimport type { Pretty, Satisfies } from \"jsr:@psk/handy/ts\";\n\ntype Input = { a: number } \u0026 { b: string };\n//     ^? { a: number } \u0026 { b: string }\n\ntype Prettified = Pretty\u003cInput\u003e;\n//     ^? { a: number; b: string }\n\ntype Str\u003cT\u003e = T extends string ? T : never;\ntype T = Satisfies\u003cStr\u003c\"ABC\"\u003e\u003e; // true\ntype F = Satisfies\u003cStr\u003c123\u003e\u003e; // false\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpskfyi%2Fhandy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpskfyi%2Fhandy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpskfyi%2Fhandy/lists"}