TypeScript
Turbine is built with TypeScript and provides full type safety throughout your application.
Instance Type
The Instance type represents the result of entity operations. Use it to type variables and function parameters:
import type { Instance } from "dynamodb-turbine";
// Get the Instance type from your entity
type User = Instance<typeof users>;
function displayUser(user: User) {
console.log(user.id);
console.log(user.email);
console.log(user.name);
}
What Instance Includes
An Instance contains:
- All schema fields - Every field from your Zod schema
- Generated key values - The actual key values stored in DynamoDB
- update() method - Convenience method for updates
const user = await users.put({
id: "123",
email: "alice@example.com",
name: "Alice",
});
// Schema fields
user.id; // string
user.email; // string
user.name; // string
// Key values (also present as attributes)
user.pk; // "user#123"
user.sk; // "alice@example.com"
// Instance method
await user.update({ name: "Alice Smith" });
Type Inference from Entities
TypeScript automatically infers types from your entity definitions:
const users = defineEntity({
table,
schema: z.object({
id: z.string(),
email: z.string().email(),
name: z.string().optional(),
role: z.enum(["user", "admin"]),
createdAt: z.string().datetime(),
}),
keys: { /* ... */ },
});
// TypeScript knows the exact shape
const user = await users.put({
id: "123",
email: "alice@example.com",
role: "admin", // Must be "user" | "admin"
createdAt: new Date().toISOString(),
});
user.role; // Type: "user" | "admin"
user.name; // Type: string | undefined
Type-Safe Operations
put()
The input type is inferred from your schema, respecting optional fields and defaults:
// Schema with defaults
schema: z.object({
id: z.string(),
name: z.string(),
role: z.enum(["user", "admin"]).default("user"),
createdAt: z.string().datetime().default(() => new Date().toISOString()),
})
// Only required fields needed
await users.put({
id: "123",
name: "Alice",
// role and createdAt have defaults, so they're optional
});
// TypeScript error: missing 'name'
await users.put({
id: "123",
});
get() and query()
Return types are automatically Instance | null or Instance[]:
const user = await users.get({ /* ... */ });
// Type: Instance<typeof users> | null
if (user) {
// TypeScript knows user is not null here
console.log(user.name);
}
const results = await users.queryAll({ /* ... */ });
// Type: Instance<typeof users>[]
results.forEach(user => {
console.log(user.email); // TypeScript knows the shape
});
update()
The patch type is a partial of your schema:
// Entity update - accepts Partial<Schema>
await users.update(
{ pk: ["user", "123"], sk: "alice@example.com" },
{ name: "Alice Smith" } // Partial<Schema>
);
// Instance update - same partial type
await user.update({
role: "admin", // Type-checked against schema
});
// TypeScript error: 'unknown' is not in schema
await user.update({
unknown: "field",
});
Using Instance in Functions
Function Parameters
type User = Instance<typeof users>;
type Post = Instance<typeof posts>;
function formatUser(user: User): string {
return `${user.name} (${user.email})`;
}
async function publishPost(post: Post): Promise<Post> {
return post.update({ status: "published" });
}
Return Types
type User = Instance<typeof users>;
async function getActiveUsers(): Promise<User[]> {
return users.queryAll({
type: "user",
}, {
filters: {
status: "active",
},
});
}
async function findUserByEmail(email: string): Promise<User | null> {
return users.queryOne({
index: "byEmail",
email,
});
}
Generic Functions
import type { Entity, Instance } from "dynamodb-turbine";
// Generic function that works with any entity
async function countItems<E extends Entity<any>>(
entity: E,
key: Parameters<E["queryAll"]>[0]
): Promise<number> {
const items = await entity.queryAll(key);
return items.length;
}
const userCount = await countItems(users, { type: "user" });
const postCount = await countItems(posts, { type: "post" });
Extracting Schema Types
You can extract the raw schema type (without Instance methods):
import { z } from "zod";
const userSchema = z.object({
id: z.string(),
email: z.string().email(),
name: z.string(),
});
// Raw schema type (for validation, forms, etc.)
type UserData = z.infer<typeof userSchema>;
const users = defineEntity({
table,
schema: userSchema,
keys: { /* ... */ },
});
// Instance type (includes update method)
type User = Instance<typeof users>;
// UserData for input validation
function validateUserInput(data: unknown): UserData {
return userSchema.parse(data);
}
// User for database operations
async function createUser(data: UserData): Promise<User> {
return users.put(data);
}
Common Patterns
Separating Entity and Types
// entities/user.ts
import { defineEntity } from "dynamodb-turbine";
import type { Instance } from "dynamodb-turbine";
import { z } from "zod";
import { table } from "./table";
export const userSchema = z.object({
id: z.string(),
email: z.string().email(),
name: z.string(),
role: z.enum(["user", "admin"]).default("user"),
createdAt: z.string().datetime().default(() => new Date().toISOString()),
});
export const users = defineEntity({
table,
schema: userSchema,
keys: {
type: () => "user",
pk: (u) => ["user", u.id],
sk: (u) => ["profile", u.email],
},
});
export type User = Instance<typeof users>;
export type UserInput = z.input<typeof userSchema>;
// services/user-service.ts
import { users, type User, type UserInput } from "../entities/user";
export async function createUser(input: UserInput): Promise<User> {
return users.put(input);
}
export async function getUserById(id: string): Promise<User | null> {
// Implementation
}
API Response Types
type User = Instance<typeof users>;
// Omit internal fields for API responses
type UserResponse = Omit<User, "pk" | "sk" | "update">;
function toResponse(user: User): UserResponse {
const { pk, sk, update, ...rest } = user;
return rest;
}
Next Steps
- Key Patterns - Design effective key structures
- Error Handling - Handle Turbine errors