@arki/ts

next

for @arki/[email protected] · Tier 2: standalone libraries · MIT

TypeScript helper types for object shapes, key casing and dotted paths, plus three tiny runtime helpers (once, lazyObject, resolveLazy) and an import-purity test helper.

Version 0.0.4 next

Install

The latest version on npm is 0.0.3 archive, not supported

bun add @arki/ts
Other package managers
npm install @arki/ts
pnpm add @arki/ts
yarn add @arki/ts

Release health for @arki/[email protected]

Doc examples

4 compiled1 run0 skipped

Open gaps

6 release checks fail

  • checkfile-llms-txt
  • checkfile-agents-md
  • checkfile-agent-guide
  • checkreadme-h2-order
  • checkpkg-engines
  • checkfiles-ship-dist-docs

Used by demos

No demo uses this package yet.

Runtimes

No engines field, so no runtime requirement is declared.

Benchmarks

No benchmarks are published for this version.

README

TypeScript helper types for object shapes, key casing and dotted paths, plus three tiny runtime helpers (once, lazyObject, resolveLazy) and an import-purity test helper. The types cost the compiler little to resolve (see Benchmarks).

Install

bun add @arki/ts
# or
npm install @arki/ts

Quick start

Read a setting by its dotted path, and let the compiler know the exact type at that path. A wrong path is a compile error.

import { expectTypeOf } from 'vitest';
 
import type { DeepPartial, PathValue, RecordToPath } from '@arki/ts';
 
const defaults = { http: { port: 8080, host: 'localhost' } } as const;
 
type SettingPath = RecordToPath<typeof defaults>; // 'http' | 'http.port' | 'http.host'
declare function getSetting<P extends SettingPath>(path: P): PathValue<typeof defaults, P>;
 
const port = getSetting('http.port'); // type: 8080
expectTypeOf(port).toEqualTypeOf<8080>();
// getSetting('http.prot'); // compile error: not a path of defaults
 
type Overrides = DeepPartial<typeof defaults>; // every level optional: { readonly http?: { readonly port?: 8080; ... } }

src/types.test-d.ts holds one expectTypeOf assertion for every helper below. The build fails when a promise in this README stops being true.

When not to use this

  • You need arrays and tuples handled inside paths ('list.0.id'). RecordToPath stops at an array: the list is a leaf.
  • You need many key styles (kebab, Pascal, constant case). DeepSnakeToCamel converts snake_case only. Use type-fest for more styles, at a higher compile cost.
  • You need branded types, schema inference or validation. Use @arki/schema.
  • You need a runtime check that a value has a shape. Types vanish at runtime. Validate with a schema.

Runtime support

The main entry (@arki/ts) and the type-only subpaths (/prettify, /utils) are portable. The only runtime code is once, resolveLazy and lazyObject, which use Proxy and no node:* imports, so they run on Node, Bun, Deno, browsers and React Native (Hermes). @arki/ts/testing is the one Node-only entry: it spawns a subprocess to check import purity. bun run test:runtimes runs a smoke test of the portable entries on each runtime. The Hermes leg needs HERMES_BIN and prints INCOMPLETE when it is missing.

RuntimeResultVersion testedDate (UTC)
nodepassv26.11.12026-10-17
bunpass1.4.22026-10-17
denopass2.9.72026-10-17
browserpassChromium 156.0.8078.42026-10-17
hermesnot run-2026-10-17

Public imports

ImportExports
@arki/tseverything below except expectImportPure
@arki/ts/utilsthe casing, path and shape types (types only)
@arki/ts/prettifyPrettify (type only)
@arki/ts/testingexpectImportPure, ImportPurityOptions (Node only)

Object shape helpers

  • Prettify<T>: flatten an intersection so the IDE shows the resolved shape.
  • DeepPartial<T>: recursively mark every property optional.
  • DeepRequired<T>: recursively mark every property required.
  • Nullable<T, K>: make selected keys nullable.
  • Without<T, W>: an Omit variant that does not flatten unions.
  • Serializable<T>: strip function members.

Casing

  • SnakeToCamel<T> and DeepSnakeToCamel<T>: convert snake_case to camelCase, one level or all levels.
  • CamelToSnake<T> and DeepCamelToSnake<T>: the reverse.

Path helpers

  • RecordToPath<T>: the union of all nested keys ('a' | 'a.b' | 'c').
  • PathValue<T, P>: the type at a dotted path.

Misc

  • Autocomplete<U, T>: a union that still autocompletes its literal members.

Runtime helpers

  • once(compute): run compute on the first call and return the same result after that.
  • lazyObject(build): an object whose construction waits for its first property access.
  • Lazy<T> and resolveLazy(value): a value or a function that returns it, and the call that resolves either.
import { lazyObject } from '@arki/ts';
 
const builds: string[] = [];
const settings = lazyObject(() => {
  builds.push('built');
  return { url: 'postgres://localhost/app' };
});
// Nothing was built when the module loaded.
const count = (): number => builds.length;
if (count() !== 0) throw new Error('built too early');
if (settings.url !== 'postgres://localhost/app') throw new Error('wrong value');
if (settings.url !== 'postgres://localhost/app' || count() !== 1) throw new Error('should build once');

Failure behaviour

The types never throw. A wrong use is a compile error. PathValue<Config, 'http.missing'> fails with TS2344, and the message lists the valid paths.

The runtime helpers throw only what your code throws:

  • once(compute): a synchronous throw is not remembered, so the next call retries. A rejected promise that compute returns is remembered. Add your own retry when an async boot may fail.
  • lazyObject(build): build runs on the first property access, so a failure appears there, not at import. The next access retries.
  • expectImportPure throws a plain Error with the subprocess output when an import reads the environment, throws or does not finish. It also throws when you pass no entry paths. The package defines no error codes.

Limits

  • RecordToPath and PathValue treat arrays as leaves. RecordToPath<{ list: { id: number }[] }> is 'list', not 'list.0'.
  • A key that contains a dot ('a.b') cannot be addressed as one segment. A path splits at the first dot.
  • DeepPartial and DeepRequired recurse into every object, including functions and class instances. Wrap those in a leaf type if you need them left alone.
  • The casing helpers work on string keys. The snake-to-camel helpers keep number and symbol keys. The camel-to-snake helpers drop them: CamelToSnake<{ userName: 1; 0: 2 }> is { user_name: 1 }.
  • Deep helpers have the compiler's recursion limit. A type with hundreds of nested levels reports TS2589.
  • lazyObject is for objects. Do not compare its result by identity with the built instance. Reflected property descriptors are configurable views of the instance; writes and deletes still obey the real descriptors. Enumeration and spreads also work with frozen instances. Defining properties or freezing the proxy itself is unsupported.
  • Lazy<T> cannot hold a function type T, because a thunk cannot be told from the value.

Documentation

@arki/ts works on its own. With the @arki/dot application framework, see packages/dot/docs/.

Support

Report a bug or ask a question at github.com/arki-labs/arki/issues. Include the type, the input and the result you expected.

Benchmarks

Type-check cost of the helpers in @arki/ts against their closest equivalents in type-fest and ts-essentials, counted with TypeScript 6.0.3. The package has no runtime code, so the cost that matters is how much work the compiler does to resolve each helper.

Measured on Node 22.13.1 (Darwin 27.0.0 arm64, Apple M2 Max (12 cores)) · @arki/bench 0.0.2 (mitata 1.0.34) · publish, seed 1592639052 · commit eeb0867c7e · 2026-10-13.

Versions: @arki/bench 0.0.2 · @arki/ts 0.0.3 · mitata 1.0.34 · ts-essentials 10.2.1 · type-fest 5.10.0.

Flatten an intersection (Prettify)

Deterministic metric in instantiations: measured once.

LibraryVersioninstantiationsvs @arki/ts Prettify
@arki/ts Prettify0.0.32,120 instantiationsbaseline
type-fest Simplify5.10.02,120 instantiations≈ same
ts-essentials Prettify10.2.12,200 instantiations1.04× higher [1.04–1.04]

Forty intersections of four object types, flattened into one object type. Checked: equivalent to the hand-written object and not an intersection.

Make every property optional, deeply (DeepPartial)

Deterministic metric in instantiations: measured once.

LibraryVersioninstantiationsvs @arki/ts DeepPartial
@arki/ts DeepPartial0.0.37,189 instantiationsbaseline
type-fest PartialDeep5.10.014,435 instantiations2.0× higher [2.0–2.0]
ts-essentials DeepPartial10.2.114,664 instantiations2.0× higher [2.0–2.0]

Five object types, four levels deep, three properties per level.

Rename snake_case keys to camelCase, deeply

Deterministic metric in instantiations: measured once.

LibraryVersioninstantiationsvs @arki/ts DeepSnakeToCamel
@arki/ts DeepSnakeToCamel0.0.310,050 instantiationsbaseline
type-fest CamelCasedPropertiesDeep5.10.033,247 instantiations3.3× higher [3.3–3.3]
ts-essentials DeepCamelCaseProperties10.2.118,679 instantiations1.9× higher [1.9–1.9]

Five object types, four levels deep, three properties per level.

List every dotted property path

Deterministic metric in instantiations: measured once.

LibraryVersioninstantiationsvs @arki/ts RecordToPath
@arki/ts RecordToPath0.0.31,814 instantiationsbaseline
type-fest Paths5.10.018,121 instantiations10.0× higher [10.0–10.0]
ts-essentials Paths10.2.110,702 instantiations5.9× higher [5.9–5.9]

Five object types, three levels deep, three properties per level (39 paths each).

Read the type at a dotted path

Deterministic metric in instantiations: measured once.

LibraryVersioninstantiationsvs @arki/ts PathValue
@arki/ts PathValue0.0.31,034 instantiationsbaseline
type-fest Get5.10.028,934 instantiations28× higher [28–28]
ts-essentials PathValue10.2.11,074 instantiations1.04× higher [1.04–1.04]

Five object types, three levels deep; each read asks for the first leaf.

Notes

  • Before counting, every library’s result is compared with a hand-written expected type: it must be assignable to it and back, and must not be any, unknown or never. The Prettify results must also be one object type, not an intersection. A mismatch stops the run.
  • The number is type instantiations: how many types the checker creates for the fixture, minus an empty file. It is deterministic for one TypeScript version and machine-independent. Check time (wall clock) is not reported.
  • @arki/ts helpers are small and cover plain objects and string keys. type-fest and ts-essentials handle more input (arrays, tuples, optional keys, unions of objects, options for how to treat them), which costs extra instantiations. A lower count here is a smaller feature set as much as it is a cheaper type.
  • DeepSnakeToCamel only converts snake_case. type-fest CamelCasedPropertiesDeep and ts-essentials DeepCamelCaseProperties accept more key styles.

Reproduce: bunx arki-bench run --profile publish in the package directory.

License

MIT. See LICENSE.

Docs 2 pages in 0.0.4

Public imports 4 paths · 32 exports

Import pathExportsFirst names
@arki/ts17 exportsAutocomplete, CamelToSnake, DeepCamelToSnake, DeepPartial, …
All 17 names
  • Autocomplete type
  • CamelToSnake type
  • DeepCamelToSnake type
  • DeepPartial type
  • DeepRequired type
  • DeepSnakeToCamel type
  • Lazy type
  • lazyObject function
  • Nullable type
  • once function
  • PathValue type
  • Prettify type
  • RecordToPath type
  • resolveLazy function
  • Serializable type
  • SnakeToCamel type
  • Without type
@arki/ts/prettify1 exportPrettify
All 1 names
  • Prettify type
@arki/ts/testing2 exportsexpectImportPure, ImportPurityOptions
All 2 names
  • expectImportPure function
  • ImportPurityOptions type
@arki/ts/utils12 exportsAutocomplete, CamelToSnake, DeepCamelToSnake, DeepPartial, …
All 12 names
  • Autocomplete type
  • CamelToSnake type
  • DeepCamelToSnake type
  • DeepPartial type
  • DeepRequired type
  • DeepSnakeToCamel type
  • Nullable type
  • PathValue type
  • RecordToPath type
  • Serializable type
  • SnakeToCamel type
  • Without type

Every export with its signature, as JSON