TypeScript
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:
TypeScript
// 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!Creating a Typed Client Wrapper #
For repeated use, create wrapper functions with your custom field types baked in:
TypeScript
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({
projectId: "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:
TypeScript
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:
TypeScript
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:
TypeScript
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";
Better I18N