TypeScript
Alinea infers TypeScript types from your schema, so there are no types to generate or keep in sync. Query results are typed automatically, and Infer gives you the types to use in your components.
Query results
The result of a query follows from its type and select:
import {cms} from '@/cms'
import {BlogPost} from '@/schema/BlogPost'
import {Query} from 'alinea'
const posts = await cms.find({
type: BlogPost,
select: {
title: Query.title,
url: Query.url,
publishDate: BlogPost.publishDate
}
})
// Array<{title: string, url: string, publishDate: string}>Without select you get all fields of the type plus the entry fields: _id, _type, _url, _locale, _parentId and so on.
To pass a result to a component, derive its type from the function that fetches it:
async function fetchPosts() {
return cms.find({
type: BlogPost,
select: {title: Query.title, url: Query.url}
})
}
type PostSummary = Awaited<ReturnType<typeof fetchPosts>>[number]Infer types from the schema
Infer turns a type, a list schema or a field into the shape queries return:
import {Config, Field, type Infer} from 'alinea'
export const TextBlock = Config.type('Text', {
fields: {
body: Field.richText('Body')
}
})
export const ImageBlock = Config.type('Image', {
fields: {
image: Field.image('Image')
}
})
export const BlogPost = Config.document('Blog post', {
fields: {
publishDate: Field.date('Publish date'),
author: Field.entry('Author'),
blocks: Field.list('Blocks', {
schema: {Text: TextBlock, Image: ImageBlock}
})
}
})
// The fields of a blog post, as a query returns them
export type BlogPost = Infer<typeof BlogPost>
// The same, plus the entry fields (_id, _type, _url, ...)
export type BlogPostEntry = Infer.Entry<typeof BlogPost>
// A row of the list field, with _id, _index and _type
export type TextBlock = Infer.ListItem<typeof TextBlock, 'Text'>
// The value of one field
export type Blocks = BlogPost['blocks']Giving the type and the constant the same name, as above, lets you import both with one name: BlogPost is the schema in queries and the inferred type in annotations.
Infer<T>: the query value of a type, a field or a list schema ({Text: TextBlock, ...}, which becomes a union of rows).Infer.Entry<T, Name>:Infer<T>plus the entry fields. Pass the type name to narrow_type.Infer.ListItem<T, Name>:Infer<T>plus the list row fields_id,_indexand_type.Infer.Stored<T>: the stored value, before links are resolved. This is whatEdit.create,Edit.updateand custom field views work with.
Narrow list rows by _type
List rows carry their type name in _type, so a switch narrows each row to its own fields:
import type {Blocks} from '@/schema/BlogPost'
export function BlocksView({blocks}: {blocks: Blocks}) {
return (
<>
{blocks.map(block => {
switch (block._type) {
case 'Text':
return <TextBlockView key={block._id} block={block} />
case 'Image':
return <img key={block._id} src={block.image?.src} alt="" />
}
})}
</>
)
}Good to know
Types describe your current schema. Content files written before you added a field don't have it yet:
alinea build --fixfills in the defaults of missing fields in every file.Infer types from the schema constants you query with. Types written by hand drift from the schema without the compiler noticing.