Runtimes

TypeScript runtime

The TypeScript runtime lives under ts/ and is distributed as @theory-cloud/tabletheory-ts. It targets Node.js 22+ (Node 22 LTS and Node 24 are exercised in CI) and the AWS SDK for JavaScript v3.

The TypeScript runtime is a peer, not a port: it implements the same P0 contract scenarios as Go and Python, and a behavior that passes the Go contract test but fails in TypeScript is a parity regression — never a “TypeScript-specific quirk.”

Install

This repo does not publish to npm. GitHub Releases are the source of truth. Install the release tarball directly:

# Stable release (replace X.Y.Z)
npm install --save-exact \
  https://github.com/theory-cloud/tabletheory/releases/download/vX.Y.Z/theory-cloud-tabletheory-ts-X.Y.Z.tgz

# Prerelease (replace X.Y.Z-rc.N)
npm install --save-exact \
  https://github.com/theory-cloud/tabletheory/releases/download/vX.Y.Z-rc.N/theory-cloud-tabletheory-ts-X.Y.Z-rc.N.tgz

@aws-sdk/client-dynamodb, @aws-sdk/client-kms, and @aws-sdk/client-sts are peer dependencies. npm 7+ installs peers automatically for ordinary installs; package managers configured with --legacy-peer-deps, some pnpm/yarn modes, or offline mirrors should install those peers explicitly with the tested ^3.1053.0 range.

The single distribution path is deliberate — it makes version drift between language registries impossible.

Find and automate release updates

GitHub Releases are the version source of truth:

gh release view --repo theory-cloud/TableTheory --json tagName,publishedAt,url
gh release list --repo theory-cloud/TableTheory --exclude-drafts --limit 10

Copy this Renovate regex manager into the consuming repository’s renovate.json to update stable TypeScript tarball URLs:

{
  "customManagers": [
    {
      "customType": "regex",
      "description": "Update TableTheory TypeScript GitHub Release asset URLs",
      "managerFilePatterns": ["/(^|/)package\\.json$/", "/(^|/)docs/.+\\.md$/"],
      "matchStrings": [
        "https://github\\.com/theory-cloud/[Tt]able[Tt]heory/releases/download/v(?<currentValue>\\d+\\.\\d+\\.\\d+)/theory-cloud-tabletheory-ts-(?<assetVersion>\\d+\\.\\d+\\.\\d+)\\.tgz"
      ],
      "datasourceTemplate": "github-releases",
      "depNameTemplate": "theory-cloud/TableTheory",
      "versioningTemplate": "semver",
      "extractVersionTemplate": "^v(?<version>\\d+\\.\\d+\\.\\d+)$",
      "autoReplaceStringTemplate": "https://github.com/theory-cloud/TableTheory/releases/download/v}/theory-cloud-tabletheory-ts-}.tgz"
    }
  ]
}

For the combined TypeScript + Python config, see Consumer update automation.

defineModel + TheorydbClient

The TypeScript public surface is explicit: models are declared via defineModel({ … }) (no decorators), and operations run through a TheorydbClient instance.

import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { TheorydbClient, defineModel } from "@theory-cloud/tabletheory-ts";

const Note = defineModel({
  name: "Note",
  table: { name: "notes_contract" },
  keys: {
    partition: { attribute: "PK", type: "S" },
    sort: { attribute: "SK", type: "S" },
  },
  attributes: [
    { attribute: "PK", type: "S", roles: ["pk"] },
    { attribute: "SK", type: "S", roles: ["sk"] },
    { attribute: "body", type: "S", optional: true, omit_empty: true },
    { attribute: "createdAt", type: "S", roles: ["created_at"] },
    { attribute: "updatedAt", type: "S", roles: ["updated_at"] },
    { attribute: "version", type: "N", roles: ["version"] },
  ],
});

const ddb = new DynamoDBClient({ region: "us-east-1" });
const db = new TheorydbClient(ddb).register(Note);

await db.create("Note", {
  PK: "USER#42",
  SK: "NOTE#welcome",
  body: "Hello, Theory Cloud.",
});

const item = await db.get("Note", { PK: "USER#42", SK: "NOTE#welcome" });

For a complete working program (model + GSI + create + query + update + delete + cursor pagination), see ts/examples/local.ts.

Role vocabulary

Every role accepted by defineModel attributes maps one-to-one onto the canonical TableTheory contract:

Go tag TypeScript role / option
theorydb:"pk" roles: ['pk']
theorydb:"sk" roles: ['sk']
theorydb:"index:gsi1,pk" roles: ['gsi1pk'] + indexes:
theorydb:"encrypted" encryption: { v: 1 }
theorydb:"version" roles: ['version']
theorydb:"created_at" roles: ['created_at']
theorydb:"updated_at" roles: ['updated_at']
theorydb:"ttl" roles: ['ttl']
theorydb:"omitempty" omit_empty: true

Naming strategy is implied by how you declare each attribute’s name — the explicit shape of defineModel means no separate “naming strategy” decoration is needed.

CRUD methods

TheorydbClient exposes the canonical CRUD surface:

await db.create('Note', { … });
const item = await db.get('Note', key);
await db.update('Note', { … }, ['body']);
await db.delete('Note', key);

const page = await db.query('Note').partitionKey('USER#42').limit(20).page();

Under DMS v0.2, a field named in the fields array is explicitly selected. If that field has omit_empty: true and its supplied value is empty, update emits DynamoDB REMOVE; fields not listed in the array remain unchanged.

Aggregation helper warning

TypeScript aggregation helpers (sum, average, min, max, aggregate, countDistinct, and groupBy) are client-side conveniences over all(). They follow every page and materialize every matching item in memory before computing the result. Use them only for bounded result sets. When the planned native count() API lands, use it instead for count-only access patterns.

Workflows

  • cd ts && npm run format:check — Prettier verification
  • cd ts && npm run lint && npm run typecheck && npm run build — CI static checks
  • cd ts && npm run test:unit — unit tests
  • cd ts && npm run test:integration — integration tests with DynamoDB Local
  • Exercised against shared contract scenarios via contract-tests/runners/ on every commit

Lambda and background work

The TypeScript runtime follows the same invariant as Go and Python: no init or handler path leaves work running after it returns. Lambda freezes the execution environment as soon as the handler returns, so a promise, timer, or socket left pending from one invocation must not be assumed to complete.

ts/src/lambda.ts therefore exposes no cold-start pre-warm. It offers:

  • isLambdaEnvironment(), createLambdaDynamoDBClient(), and getLambdaDynamoDBClient() — synchronous client construction with keep-alive agents, reused across warm invocations
  • createLambdaTimeoutSignal() and withLambdaTimeout() — a per-invocation abort signal. The watchdog timer is unref()-ed, so it never holds the event loop open, and cleanup() clears it

Parallel fan-out is allowed only when it is fully joined. Query.scanAllSegments() runs one worker per segment through mapConcurrent, which waits for every worker to settle before it rejects: a failing segment aborts the remaining scans, and the original failure is re-thrown only after the other workers have finished, so no segment work outlives the call. ts/test/unit/query-builder.test.ts covers that error path.

Where to go next

Stability and support

The TypeScript runtime is GA. Versions are aligned with Go and Python per the multi-language version-sync invariant. Breaking changes ship coordinated with the other two runtimes — never in isolation.