Type System
Understand How Field Selection Drives Return Types
The SDK generates TypeScript types per collection. When you specify fields, the return type narrows to include only those fields. When you omit fields, you get all scalar fields.
By default, strictNull: true adds | null to every field — see Null Strictness for details.
const articles = await client.Articles.readMany({
fields: ['*'],
});
// Inferred type:
// {
// id: string | null;
// title: string | null;
// status: string | null;
// content: string | null;
// author_id: number | null;
// created_at: string | null;
// }[]
Specify fields and the return type shrinks to match:
const articles = await client.Articles.readMany({
fields: ['id', 'title', 'status'],
});
// Inferred type:
// {
// id: string | null;
// title: string | null;
// status: string | null;
// }[]
A single item works the same way:
const article = await client.Articles.readOne({
key: 1,
fields: ['id', 'title'],
});
// Inferred type:
// {
// id: string | null;
// title: string | null;
// }
This automatic narrowing means your code is always type-safe against the exact shape of data you requested. If you remove a field from your fields array, TypeScript immediately catches any code that references it. If you add a relational field, the nested type is automatically available. This eliminates an entire category of runtime errors.
This is the core mechanism — every example that follows builds on it.
Select Fields from Nested Relations
Request related data by nesting field arrays inside objects. The inferred type reflects the relationship cardinality.
To-one relations return T | null. To-many relations return { data: T[] } — a data envelope wrapping the array.
const articles = await client.Articles.readMany({
fields: ['id', 'title', { author: ['name', 'email'] }],
});
// Inferred type:
// {
// id: string | null;
// title: string | null;
// author: {
// name: string | null;
// email: string | null;
// } | null;
// }[]
const authors = await client.Authors.readMany({
fields: ['id', 'name', { articles: ['title', 'status'] }],
});
// Inferred type:
// {
// id: string | null;
// name: string | null;
// articles: {
// data: {
// title: string | null;
// status: string | null;
// }[];
// } | null;
// }[]
T | null because the foreign key may be null (or stripped by permissions). To-many returns { data: T[] } | null — a data envelope that is itself nullable under strictNull: true. With strictNull: false, the envelope becomes non-nullable and to-one nullability follows the schema.Filter, Sort, and Paginate Nested Relations
Use expanded field selection to filter, sort, and limit nested relations. This is the SDK equivalent of the REST deep parameter.
For to-many relations, pass an object with fields, filter, sort, limit, and offset:
const authors = await client.Authors.readMany({
fields: ['id', 'name', {
articles: {
fields: ['title', 'status'],
filter: { status: { _eq: 'published' } },
sort: [{ created_at: { direction: 'desc' } }],
limit: 5,
},
}],
});
// Inferred type:
// {
// id: string | null;
// name: string | null;
// articles: {
// data: {
// title: string | null;
// status: string | null;
// }[];
// } | null;
// }[]
curl -g "https://example.monospace.io/api/blog/items/authors?fields=id,name,articles.title,articles.status&deep[articles][_filter][status][_eq]=published&deep[articles][_sort][0][created_at][direction]=desc&deep[articles][_limit]=5" \
-H "Authorization: Bearer YOUR_API_KEY"
const params = new URLSearchParams({
fields: 'id,name,articles.title,articles.status',
'deep[articles][_filter][status][_eq]': 'published',
'deep[articles][_sort][0][created_at][direction]': 'desc',
'deep[articles][_limit]': '5',
});
const response = await fetch(
`https://example.monospace.io/api/blog/items/authors?${params}`,
{
headers: {
Authorization: 'Bearer YOUR_API_KEY',
},
},
);
const { data } = await response.json();
For to-one relations, sort, limit, and offset are not available — only fields and filter:
const articles = await client.Articles.readMany({
fields: ['id', 'title', {
author: {
fields: ['name'],
filter: { active: { _eq: true } },
},
}],
});
// Inferred type:
// {
// id: string | null;
// title: string | null;
// author: {
// name: string | null;
// } | null;
// }[]
curl -g "https://example.monospace.io/api/blog/items/articles?fields=id,title,author.name&deep[author][_filter][active][_eq]=true" \
-H "Authorization: Bearer YOUR_API_KEY"
const params = new URLSearchParams({
fields: 'id,title,author.name',
'deep[author][_filter][active][_eq]': 'true',
});
const response = await fetch(
`https://example.monospace.io/api/blog/items/articles?${params}`,
{
headers: {
Authorization: 'Bearer YOUR_API_KEY',
},
},
);
const { data } = await response.json();
deep parameter — see Field Selection.Define Reusable Queries with satisfies
Extract query parameters into a variable for reuse. Use satisfies to validate the shape at compile time while preserving literal types for precise return type inference.
import type { ArticleReadManyParameters } from './generated/monospace';
const publishedArticles = {
fields: ['id', 'title', 'status', {
author: {
fields: ['name'],
filter: { active: { _eq: true } },
},
}] as const,
filter: { status: { _eq: 'published' } },
sort: [{ created_at: { direction: 'desc' as const } }],
limit: 50,
} satisfies ArticleReadManyParameters;
const articles = await client.Articles.readMany(publishedArticles);
// Inferred type:
// {
// id: string | null;
// title: string | null;
// status: string | null;
// author: {
// name: string | null;
// } | null;
// }[]
satisfies instead of a : Type annotation — it preserves literal types for precise return type inference. A : ArticleReadManyParameters annotation widens fields to string[], losing track of which fields were selected.Type Function Arguments
Use the generated parameter types to type function arguments. This gives callers type-safe query building and return type inference.
import type { ArticleReadManyParameters } from './generated/monospace';
async function fetchArticles<const P extends ArticleReadManyParameters>(params: P) {
return client.Articles.readMany(params);
}
const articles = await fetchArticles({
fields: ['id', 'title'],
filter: { status: { _eq: 'published' } },
});
// Inferred type:
// {
// id: string | null;
// title: string | null;
// }[]
Every operation has a matching parameter type: ArticleReadOneParameters, ArticleCreateOneParameters, ArticleUpdateOneParameters, and so on.
Understand Parameters vs Args Types
Each collection generates two related type families per operation:
{Collection}{Op}Parameters— query string parameters only:fields,filter,sort,limit,offset. Useful for composing reusable query parts that can be spread into different calls.{Collection}{Op}Args— the full argument object, includingdatafor mutations andkeyfor single-item operations. This is what the SDK methods actually accept.
Generally prefer Args as it's the more complete type. Parameters is useful when you want to define reusable query fragments independently of a specific operation call.
import type { AuthorReadOneParameters, AuthorReadOneResultItem } from './generated/monospace';
const queryParams = {
fields: ['id', 'first_name', 'last_name'],
} satisfies AuthorReadOneParameters;
type Author = AuthorReadOneResultItem<typeof queryParams>;
function readAuthor(key: string): Promise<Author> {
return client.Author.readOne({ key, ...queryParams });
}
Extract Result Types for Component Props
Use the generated result types to type component props, function return values, or any variable that holds query results.
{Collection}{Op}Result gives you the full result type — an array for readMany, a single object for readOne. {Collection}{Op}ResultItem always gives you the single-item shape, which is useful for typing component props.
import type { ArticleReadManyResult, ArticleReadManyResultItem } from './generated/monospace';
// Full result type (array for readMany)
type ArticleList = ArticleReadManyResult<{
fields: ['id', 'title', 'status'];
}>;
// = { id: string | null; title: string | null; status: string | null }[]
// Single item type (useful for component props)
type ArticleCard = ArticleReadManyResultItem<{
fields: ['id', 'title', 'status'];
}>;
// = { id: string | null; title: string | null; status: string | null }
ArticleReadOneResult, ArticleReadOneResultItem, ArticleCreateOneResult, and so on.Type Create and Update Payloads
The SDK generates input types for create and update operations. Create inputs require all non-nullable fields. Update inputs make every field optional.
import type { ArticleCreateOneInput } from './generated/monospace';
const newArticle: ArticleCreateOneInput = {
title: 'Getting Started', // required — non-nullable field
status: 'draft', // optional
author_id: 1, // optional
};
await client.Articles.createOne({
data: newArticle,
fields: ['id', 'title'],
});
import type { ArticleUpdateOneInput } from './generated/monospace';
const changes: ArticleUpdateOneInput = {
status: 'published', // only include fields you want to change
};
await client.Articles.updateOne({
key: 1,
data: changes,
fields: ['id', 'status'],
});
For relational writes using nested create and update syntax, see Relational Data.
Type Primary Key Parameters
Each collection exports a key type that matches its primary key field. Use it to type variables and function parameters that hold item IDs.
import type { ArticleKey, AuthorKey } from './generated/monospace';
// ArticleKey = string (uuid primary key)
// AuthorKey = number (auto-increment primary key)
async function getArticle(id: ArticleKey) {
return client.Articles.readOne({
key: id,
fields: ['id', 'title'],
});
}
The key type is string or number depending on your schema's primary key field type.
See Also
- Field Selection — REST
fieldsanddeepparameter reference - Client Setup — SDK installation and
strictNullconfiguration - Relational Data — nested create and update input types
- Advanced — null strictness, per-query overrides, and more