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 verificationcd ts && npm run lint && npm run typecheck && npm run build— CI static checkscd ts && npm run test:unit— unit testscd 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(), andgetLambdaDynamoDBClient()— synchronous client construction with keep-alive agents, reused across warm invocationscreateLambdaTimeoutSignal()andwithLambdaTimeout()— a per-invocation abort signal. The watchdog timer isunref()-ed, so it never holds the event loop open, andcleanup()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
- Getting Started — full walkthrough
ts/docs/getting-started.md— TypeScript-specific runtime documentationts/docs/api-reference.md— full TypeScript API reference- Features → CRUD & Marshaling, Optimistic Locking, Encryption
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.