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 | nullStatus 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";