Skip to content

API Reference

/

Data types

Table

The Table class is a reference to a database table. It can optionally carry the type of the records stored in the table, so that TypeScript type checks query results and the data you write.

Import:

import { Table } from 'surrealdb';

Source: value/table.ts

  • Tb - The table name, as a string literal type (optional, defaults to string)

  • T - The type of the records in this table (optional, defaults to unknown)

Note

T is a compile-time annotation only. It is not validated against the data returned by the server.

Create a new table reference.

Syntax
new Table(name)
new Table<RecordType>(name)
new Table<"name", RecordType>(name)

Parameter

Type

Description

namestring

The table name.

// Basic table reference
const users = new Table('users');

// Table reference with a record type
interface User {
    id: RecordId;
    name: string;
    email: string;
}

const users = new Table<User>('users');

// TypeScript now knows the result type
const allUsers: User[] = await db.select(users);

There are four ways to give a table a record type. They differ in whether the table name is kept as a literal type ("users") or widened to string.

// 1. Record type only
const users = new Table<User>('users');                    // Table<string, User>

// 2. Table.of: keeps the literal name and the record type
const users = Table.of<User>()('users');                   // Table<"users", User>

// 3. Annotated variable: the name is inferred, the record type comes from the annotation
const users: Table<'users', User> = new Table('users');    // Table<"users", User>

// 4. Both type arguments, explicitly
const users = new Table<'users', User>('users');           // Table<"users", User>

Form 1 is the shortest and is enough for most code. Use forms 2 to 4 if you need the table name as a literal type, for example in a function that only accepts Table<'users', User>. A table declared with form 1 is Table<string, User>, which can't be passed where a Table<'users', User> is expected.

In forms 3 and 4, a table name or record type that doesn't match the annotation is a compile error:

const users: Table<'user', User> = new Table('users');    // Error: "users" is not "user"

A table created without a record type, such as new Table('users'), is Table<'users', unknown>. You can still give the record type on the call instead:

const allUsers = await db.select<User>(new Table('users'));

The record type is picked up by select, create, update, upsert, delete and insert.

A RecordId or RecordIdRange created from a typed table keeps the record type, so the methods that take one infer it as well:

const users = Table.of<User>()('users');
const id = new RecordId(users, 'john');   // RecordId<"users", string, User>
const range = new RecordIdRange(users, new BoundIncluded(1), new BoundExcluded(5));

const user = await db.select(id);         // User | undefined (with `id`)
const some = await db.select(range);      // User[] (with `id`)

Both take the record type as a third type parameter, RecordId<Tb, Id, T> and RecordIdRange<Tb, Id, T>. There is no new RecordId<User>(...) shorthand: take the record type from a typed table, or annotate the variable, as in const id: RecordId<'users', string, User> = new RecordId('users', 'john').

Note

Passing an explicit type that differs from the table's own record type, such as db.select<Post>(users) where users is a Table<'users', User>, is not an error: the explicit type takes precedence. Prefer letting the type come from the table.

The table name.

Type: Tb (the table name type, string unless declared as a literal)

const users = new Table('users');
console.log(users.name); // 'users'

Convert to string representation (escaped table name).

Syntax
table.toString()

string - Escaped table name

const users = new Table('users');
console.log(users.toString()); // 'users'

// Handles special characters
const special = new Table('my-table');
console.log(special.toString()); // Properly escaped

Serialise for JSON.

Syntax
table.toJSON()

string - Table name

Check if two table references are equal.

Syntax
table.equals(other)

boolean - True if equal

import { Surreal, Table } from 'surrealdb';

// Define your data model
interface User {
    id: RecordId;
    name: string;
    email: string;
    age: number;
}

interface Post {
    id: RecordId;
    title: string;
    content: string;
    author: RecordId;
}

const db = new Surreal();
await db.connect('ws://localhost:8000');

// Create typed table references
const users = new Table<User>('users');
const posts = new Table<Post>('posts');

// TypeScript knows the return types
const allUsers: User[] = await db.select(users);
const allPosts: Post[] = await db.select(posts);

// Type checking in IDE
allUsers[0].name; // ✓ TypeScript knows this exists
allUsers[0].title; // ✗ TypeScript error: Property 'title' does not exist
const users = new Table<User>('users');

// Select all records
const all = await db.select(users);

// Select with filtering
const active = await db.select(users)
    .where('active = true');

// Select with pagination
const page = await db.select(users)
    .limit(10)
    .start(0);
const users = new Table<User>('users');

// Create single record with auto-generated ID
const user = await db.create(users).content({
    name: 'John Doe',
    email: 'john@example.com',
    age: 30
});

// Create multiple records
const newUsers = await db.insert(users, [
    { name: 'Alice', email: 'alice@example.com', age: 25 },
    { name: 'Bob', email: 'bob@example.com', age: 35 }
]);
const users = new Table<User>('users');

// Update all records in table
const updated = await db.update(users)
    .merge({ verified: true })
    .where('email_confirmed = true');

// Update with content
await db.update(users)
    .content({
        name: 'New Name',
        email: 'new@example.com',
        age: 31
    })
    .where('id = users:john');
const users = new Table<User>('users');

// Delete all inactive users
const deleted = await db.delete(users)
    .where('inactive = true');

// Get deleted records
const deletedWithData = await db.delete(users)
    .output('BEFORE')
    .where('created_at < $date', { 
        date: DateTime.parse('2024-01-01') 
    });
interface Product {
    id: RecordId;
    name: string;
    price: Decimal;
}

interface Order {
    id: RecordId;
    user: RecordId;
    products: RecordId[];
    total: Decimal;
}

const products = new Table<Product>('products');
const orders = new Table<Order>('orders');
const users = new Table<User>('users');

// Work with multiple tables
const [allProducts, allOrders, allUsers] = await Promise.all([
    db.select(products),
    db.select(orders),
    db.select(users)
]);
const users = new Table<User>('users');

// Build complex queries
const query = db.select(users)
    .fields('name', 'email', 'age')
    .where('age >= $minAge', { minAge: 18 })
    .order('name ASC')
    .limit(100);

const results = await query;
const users = new Table<User>('users');

// Subscribe to table changes
const subscription = await db.live(users);

for await (const update of subscription) {
    if (update.action === 'CREATE') {
        console.log('New user:', update.result);
    } else if (update.action === 'UPDATE') {
        console.log('User updated:', update.result);
    } else if (update.action === 'DELETE') {
        console.log('User deleted:', update.result);
    }
}

// Clean up
await subscription.kill();
const users = new Table<User>('users');
const orders = new Table<Order>('orders');

const txn = await db.beginTransaction();

try {
    // Create user
    const user = await txn.create(users).content({
        name: 'Jane Doe',
        email: 'jane@example.com'
    });
    
    // Create order
    await txn.create(orders).content({
        user: user.id,
        products: [],
        total: new Decimal('0')
    });
    
    await txn.commit();
} catch (error) {
    await txn.cancel();
}
// Create reusable functions
// `Table<string, T>` accepts any table, and picks up its record type
async function getAllFromTable<T>(
    db: Surreal,
    table: Table<string, T>
): Promise<T[]> {
    return db.select(table);
}

async function deleteAllFromTable<T>(
    db: Surreal,
    table: Table<string, T>
): Promise<T[]> {
    return db.delete(table);
}

// Use with any table
const users = await getAllFromTable(db, new Table<User>('users'));
const posts = await getAllFromTable(db, new Table<Post>('posts'));
// Define table with strict types
interface StrictUser {
    id: RecordId<'users', string>;
    name: string;
    email: string;
    age: number;
    created_at: DateTime;
    updated_at: DateTime;
}

const users = new Table<StrictUser>('users');

// TypeScript enforces the schema
const user = await db.create(users).content({
    name: 'Alice',
    email: 'alice@example.com',
    age: 25,
    created_at: DateTime.now(),
    updated_at: DateTime.now()
    // Missing fields or wrong types will cause TypeScript errors
});
class UserRepository {
    private table = new Table<User>('users');
    
    constructor(private db: Surreal) {}
    
    async getAll(): Promise<User[]> {
        return this.db.select(this.table);
    }
    
    async getById(id: RecordId): Promise<User | undefined> {
        return this.db.select(id);
    }
    
    async create(data: Omit<User, 'id'>): Promise<User> {
        return this.db.create(this.table).content(data);
    }
    
    async update(id: RecordId, data: Partial<User>): Promise<User> {
        return this.db.update(id).merge(data);
    }
    
    async delete(id: RecordId): Promise<void> {
        await this.db.delete(id);
    }
    
    async findByEmail(email: string): Promise<User[]> {
        return this.db.select(this.table)
            .where('email = $email', { email });
    }
}

// Usage
const userRepo = new UserRepository(db);
const users = await userRepo.getAll();
const john = await userRepo.findByEmail('john@example.com');
// Good: Type-safe
const users = new Table<User>('users');
const results = await db.select(users); // User[] (with `id`)

// Avoid: No record type
const users = new Table('users');
const results = await db.select(users); // unknown[]

If you need the table name as a literal type as well, see Declaring a typed table.

// Good: Clear data structure
interface User {
    id: RecordId;
    name: string;
    email: string;
    created_at: DateTime;
}

const users = new Table<User>('users');

// Avoid: Using 'unknown' or no interface
const users = new Table('users');
// Good: Single source of truth
const USERS_TABLE = new Table<User>('users');

async function getUsers() {
    return db.select(USERS_TABLE);
}

async function createUser(data: Omit<User, 'id'>) {
    return db.create(USERS_TABLE).content(data);
}

// Avoid: Creating new references everywhere
async function getUsers() {
    return db.select(new Table('users'));
}
// Good: Strongly typed
interface User {
    id: RecordId<'users', string>;
    name: string;
}

const users = new Table<User>('users');

// TypeScript ensures correct table name
const userId: RecordId<'users', string> = new RecordId('users', 'john');
enum Tables {
    Users = 'users',
    Posts = 'posts',
    Comments = 'comments'
}

const users = new Table<User>(Tables.Users);
const posts = new Table<Post>(Tables.Posts);
function createTable<T>(name: string): Table<string, T> {
    return new Table<T>(name);
}

const users = createTable<User>('users');
const posts = createTable<Post>('posts');

Was this page helpful?