# Bug 3103: createTableSchema cannot handle recursive types as arguments

- Status: closed
- Project: Zero
- Creator: @aboodman
- Assignee: @tantaman
- Created: 2024-10-29T23:01:18Z
- Modified: 2025-02-02T03:12:39Z
- Reactions: ➕️ ×2 (hubciorz, jorroll) · 😗 ×3 (KATT, TamirShklaz, hubciorz)
- URL: https://bugs.rocicorp.dev/p/zero/issue/3103

## Description

See: https://imgur.com/a/2qx0KEX

It still works if you take off the `createTableSchema` helper and define the schema manually.

## Comments (10)

### @aboodman — 2024-10-31T03:14:44Z

I think that probably what we should do here is replace the function call version of `schema` with a special `"self"` sentinel:

```ts
- schema: TDestSchema | Lazy<TDestSchema>;
+ schema: TDestSchema | 'self';
```

I can't think of any other way to get type hints when declaring these things and also have recursive definitions.

I think handling the runtime part of this will be easyish but still working through the compiletime part of it.

### @arv — 2024-10-31T11:32:47Z

The way it was before worked in cases like these:

```ts
const viewStateSchema = {
  tableName: 'viewState',
  columns: {
    issueID: {type: 'string'},
    userID: {type: 'string'},
    viewed: {type: 'number'},
  },
  primaryKey: ['issueID', 'userID'],
  relationships: {},
} as const satisfies TableSchema;
```

In other words, using `as const satisfies TableSchema`.

Not half as pretty though.

### @aboodman — 2024-10-31T18:54:34Z

Sorry Erik I don't follow. What I'm trying to do is pass a schema definition object to `createTableSchema` that contains a reference to itself.

We already had this working without `createTableSchema`, but with `createTableSchema` it fails. ChatGPT claims this is because inference gets more complicated when there is a 'receiving type'.

### @tantaman — 2024-11-01T17:19:10Z

I'll spend some cycles on this while I move the auth rules to the schema today.

Reactions: 🙏 ×1 (aboodman)

### @tantaman — 2024-11-26T19:30:02Z

this appears to be impossible without forcing the user to write the actual typescript type themselves. Valita & Zod suffer from the same problem as seen in our AST schemas.

### @nandorojo — 2024-12-16T14:22:03Z

One option might be to use Omit<> internally for self referencing. Zod enables self referencing with lazy() I believe

### @tantaman — 2024-12-16T15:43:12Z

Yeah, we have a `lazy` option and do use that for recursive schemas but you still end up with TypeScript barfing if you try to call `createTableSchema`. Zod has the same issue and requires you to define the Typescript type separately from the Zod schema in those cases.

### @tantaman — 2025-01-07T17:41:24Z

ok, so seems like we can fix all this by:

1. Splitting the API into `table` and `relationships`
2. Having two parallel structures: `AllTables` and `AllRelationships` that do not reference one another except by having the same `TableName`
3. Update the `Query` interface to be generic on: `TableName, FullSchema`

```ts
interface Query<TTableName extends string, TFullSchema extends FullSchema>;

type FullSchema = {
  allTables: { [tableName: string]: TableSchema}
  allRelationships: { [tableName: string]: RelationshipsSchema }
}

const userSchema = table('user').columns(...);
const userRelationships = relationships('user', {...});
```

### @tantaman — 2025-01-07T17:43:47Z

the `omit` trick works (and is simple to implement) but isn't ideal since it requires the user to be aware of two different APIs. 
The high level `createTable` (or future builder API) and the low-level structure-as-const.

### @tantaman — 2025-01-22T19:58:28Z

Fixed by: https://github.com/rocicorp/mono/pull/3545/
