Skip to content

Conversation

@turadg
Copy link
Member

@turadg turadg commented Oct 1, 2025

no issue

Description

In Agoric-SDK we often have to turn a TypeScript type into an Endo Pattern, or vice-versa. In YMax work I'm doing the same with Zod schemas.

I'd like the LLMs to do this for me so here is some documentation they can use. It also has unit tests to validate the examples.

Security Considerations

none

Scaling Considerations

none

Documentation Considerations

per se

Testing Considerations

included

Compatibility Considerations

Does this change break any prior usage patterns? Does this change allow usage patterns to evolve?

Upgrade Considerations

n/a


The examples demonstrate both successful and unsuccessful conversions. The tests in [`../../test/rosetta/examples.test.js`](../../test/rosetta/examples.test.js) exercise these snippets. They prove the conversions that work today and highlight the known mismatches when a translation is not possible.

> **Note:** Zod is an optional development dependency. Install it within this repository (`yarn workspace @endo/patterns add --dev zod`) before running the Rosetta tests locally.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this somehow more optional than other devDependencies in package.json? For other devDependencies, I just depend on normal yarn && yarn build to install so I can use them for local development. What makes this one different.

(I'm not opposed to this one being more optional. I just don't understand what the mechanism is.)

| Pattern | Suggested TypeScript | Notes |
| --- | --- | --- |
| `M.string()` | `type T = string;` | Strings translate directly. |
| `M.number()` | `type T = number;` | TypeScript cannot exclude `NaN` or `Infinity` at the type level; rely on runtime validation. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

M.number() does no range checking. Did you mean

Suggested change
| `M.number()` | `type T = number;` | TypeScript cannot exclude `NaN` or `Infinity` at the type level; rely on runtime validation. |
| `M.number()` | `type T = number;` | |
| `M.nat()` | `type T = number;` | TypeScript cannot exclude `NaN` or `Infinity` at the type level; rely on runtime validation. |

Comment on lines +10 to +14
| `M.splitRecord({ id: M.nat() }, { note: M.string() })` | ```ts
interface RecordShape {
id: bigint;
note?: string;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know how markdown tables relate to markdown triple backtick. But this is not rendering correctly.

Ignoring the backtick issue I don't know how to solve, you could also extend this in place, or perhaps include as a distinct line, for some suitable T

Suggested change
| `M.splitRecord({ id: M.nat() }, { note: M.string() })` | ```ts
interface RecordShape {
id: bigint;
note?: string;
}
| `M.splitRecord({ id: M.nat() }, { note: M.string() }, T)` | ```ts
interface RecordShape {
id: bigint;
note?: string;
...T;
}

@@ -0,0 +1,27 @@
# Relating Endo Patterns to TypeScript Types
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do these relate to the types you already infer from patterns and guards?

id: bigint;
note?: string;
}
``` | TypeScript `bigint` does not encode non-negativity. Convey the constraint in docs or branded types. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue. M.bigint() also does not encode non-negativity

note?: string;
}
``` | TypeScript `bigint` does not encode non-negativity. Convey the constraint in docs or branded types. |
| `M.arrayOf(M.string())` | `type T = string[];` | Combine with tuple types if `arrayLengthLimit` is finite. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does "Combine with tuple types" mean here?

Comment on lines +18 to +19
| `M.setOf(M.string())` | `type T = ReadonlyArray<string>;` | TypeScript lacks a native `CopySet`. Express as `ReadonlyArray` with documentation noting deduplication. |
| `M.bagOf(M.string())` | `type T = ReadonlyArray<[string, bigint]>;` | A bag is best expressed as entries with counts. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For setOf, bagOf, and the missing mapOf, typescript can type the enclosing envelope record, including the literal tag value ('set', 'bag', or 'map'). In fact, I thought you already created the typescript types for these.

| `M.arrayOf(M.string(), harden({ arrayLengthLimit: 2 }))` | `type T = readonly [string?, string?];` | Endo can enforce maximum length at runtime. A tuple with optional slots expresses the same upper bound statically. |
| `M.setOf(M.string())` | `type T = ReadonlyArray<string>;` | TypeScript lacks a native `CopySet`. Express as `ReadonlyArray` with documentation noting deduplication. |
| `M.bagOf(M.string())` | `type T = ReadonlyArray<[string, bigint]>;` | A bag is best expressed as entries with counts. |
| `M.promise()` | `type T = Promise<unknown>;` | Add generics for resolved types if known; runtime checks must ensure the promise is handled according to Endo’s pass-style rules. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand why statements about runtime checks are relevant to patterns/guard and to zod. But what does this mean as applied to TypeScript?

Comment on lines +21 to +23
| `M.interface({ getValue: M.callWhen([M.number()], M.number()) })` | ```ts
interface ValueService {
getValue(input: number): number;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| `M.interface({ getValue: M.callWhen([M.number()], M.number()) })` | ```ts
interface ValueService {
getValue(input: number): number;
| `M.interface({ getValue: M.callWhen([M.number()], M.number()) })` | ```ts
interface ValueService {
async getValue(input: number): number;

A callWhen corresponds to an async method declaration.

interface ValueService {
getValue(input: number): number;
}
``` | TypeScript describes the shape but cannot enforce async/await behaviour. The Endo guard still provides runtime enforcement. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, what does it mean to even talk about enforcement when talking about TypeScript (as opposed to patterns/guards and to zod)?

M.await(M.number()) still has the typing issue that the external type should be Promise<number> while the internal type should be number

Copy link
Member

@kriskowal kriskowal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I defer to @erights for the review as a subject matter expert, but if the resulting docs are also useful to humans (they look useful to humans), consider incorporating them in typedoc. That may entail moving them to top-level docs, adding YAML front-matter to give the document a title consistent with the convention in neighboring docs, and adding it to its place in order in typedoc.json. The result would be that it would be prominent on https://docs.endojs.org. Consequently, any disagreement about how to render Markdown should be settled toward Typedoc’s rendering over Github’s, if both cannot be satisfied.


`M` also has {@link GuardMakers} methods to make {@link InterfaceGuard}s that use Patterns to characterize dynamic behavior such as method argument/response signatures and promise awaiting. The {@link @endo/exo!} package uses `InterfaceGuard`s as the first level of defense for Exo objects against malformed input.

If you need to translate between Endo Patterns, Zod schemas, and TypeScript declarations, start with the [Rosetta guide](./docs/rosetta/index.md). It collects executable examples together with notes about fidelity gaps and recommended work-arounds.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest making Zod a link. Also would be handy to call-out which Rosetta this is referring to.

}
``` | TypeScript describes the shape but cannot enforce async/await behaviour. The Endo guard still provides runtime enforcement. |
When a runtime constraint has no native static representation, consider branded types: e.g., `type Nat = bigint & { readonly brand: unique symbol; }`. Guard constructors can cast the value after validation.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain this so a non-TS expert like me can understand this?

@turadg
Copy link
Member Author

turadg commented Oct 2, 2025

I'm going to put this on the backburner in favor of Agoric/agoric-sdk#12044

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants