Better I18NBetter I18N

TypeScript

Type-safe content with generic custom fields

The Content SDK is built with TypeScript and provides full type safety for all responses, including support for typed custom fields via generics.

Generic Custom Fields

Content models can define custom fields (e.g., readingTime, category). Custom fields are spread flat onto the entry object — there is no nested customFields wrapper. By default, additional fields are typed as Record<string, string | null>. Use the generic type parameter on .single<CF>() to get exact typing:

// Define your custom fields interface
interface BlogFields {
  readingTime: string | null;
  category: string | null;
  featured: string | null;
}

// Pass it to single()
const { data: post } = await client
  .from("blog-posts")
  .single<BlogFields>("hello-world");

post.readingTime; // string | null (typed!)
post.category;    // string | null (typed!)
post.featured;    // string | null (typed!)
post.unknown;     // TypeScript error!

Custom fields are flat on the entry — access them as post.readingTime, not post.customFields.readingTime.

Creating a Typed Client Wrapper

For repeated use, create wrapper functions with your custom field types baked in:

import { createClient, type ContentEntry, type QueryResult, type ContentEntryListItem, type SingleQueryResult } from "@better-i18n/sdk";

interface BlogFields {
  readingTime: string | null;
  category: string | null;
}

const client = createClient({
  project: "acme/web-app",
  apiKey: process.env.BETTER_I18N_API_KEY!,
});

export async function getBlogPost(slug: string, language?: string): Promise<SingleQueryResult<ContentEntry<BlogFields>>> {
  return client
    .from("blog-posts")
    .language(language ?? "en")
    .single<BlogFields>(slug);
}

export async function listBlogPosts(page = 1): Promise<QueryResult<ContentEntryListItem<BlogFields>[]>> {
  return client
    .from("blog-posts")
    .eq("status", "published")
    .order("publishedAt", { ascending: false })
    .limit(10)
    .page(page);
}

Usage stays clean and fully typed:

const { data: post } = await getBlogPost("hello-world", "en");
post?.readingTime; // string | null
post?.category;    // string | null

const { data: posts, total } = await listBlogPosts(1);
posts?.[0].readingTime; // string | null

Status Type Narrowing

The status field is a union type, enabling exhaustive checks:

import type { ContentEntryStatus } from "@better-i18n/sdk";

function getStatusLabel(status: ContentEntryStatus): string {
  switch (status) {
    case "draft":
      return "Draft";
    case "published":
      return "Published";
    case "archived":
      return "Archived";
  }
}

Exported Types

All types are re-exported from the package root:

import type {
  // Configuration
  ClientConfig,
  ContentClient,

  // Query builder classes
  ContentQueryBuilder,
  SingleQueryBuilder,

  // Content types
  ContentModel,
  ContentEntry,
  ContentEntryListItem,
  ContentEntryStatus,
  ContentEntrySortField,
  RelationValue,

  // Response types
  QueryResult,
  SingleQueryResult,
} from "@better-i18n/sdk";

On this page