Extending Payload Types
*GetPayload mirrors your Strapi schema exactly. When a controller returns more than the schema describes — computed fields, narrower variants of an existing field — a direct as Domain cast won't compile:
type Share = Omit<
ProjectGetPayload<{ populate: { config: true } }>,
'config'
> & {
config: ProjectShareConfig[]
watermark: boolean
authorUsername: string
}
const data = (await strapi.projects.findOne(documentId, {
populate: { config: true },
})) as Share
// TS2352: neither type sufficiently overlaps with the other.Omit removes config, the intersection adds a different config, and the resulting Share is no longer a subtype of ProjectGetPayload. TypeScript correctly refuses the cross-cast. The recipes below cover the practical ways to bridge the gap.
Spread With Per-Field Cast
For most cases this is the shortest path. Spread the payload, declare the additions and narrowings explicitly:
const data = await strapi.projects.findOne(documentId, {
populate: { item: { populate: ['category'] }, config: true },
})
if (!data) return null
const share: Share = {
...data,
watermark: computeWatermark(data),
authorUsername: getAuthorUsername(data),
refCode: getRefCode(data),
config: data.config as ProjectShareConfig[],
}The cast is narrow and local to the one field that genuinely diverges from the schema. TypeScript still checks every other field against Share.
Intersection When You Only Add Fields
If the domain type purely adds fields — no narrowing of existing ones — a plain intersection compiles without any cast:
type ShareLite = ProjectPayload & {
watermark: boolean
authorUsername: string
}
const share = (await strapi.projects.findOne(...)) as ShareLiteShareLite is a subtype of ProjectPayload, so as is valid.
as unknown as Domain at the Boundary
When the gap is large enough that spread feels awkward, a double cast at the repository boundary is a fine pragmatic choice:
async function findShare(documentId: string): Promise<Share | null> {
const data = await strapi.projects.findOne(documentId, {
populate: { item: { populate: ['category'] }, config: true },
})
return data as unknown as Share
}Keep it at the boundary (one place per domain type, in the repository or data-access layer). The signature of findShare carries the Share contract from that point on, so the rest of the codebase stays cast-free.
Mapper Function When You Want Explicitness
If you'd rather see every domain field assigned by hand — useful when the mapping is non-trivial or when reviewers benefit from the cross-reference — write a mapper:
function toShare(
payload: ProjectPayload,
computed: { watermark: boolean; authorUsername: string; refCode?: string },
): Share {
return {
documentId: payload.documentId,
item: payload.item,
config: payload.config as ProjectShareConfig[],
...computed,
}
}This makes schema drift impossible to miss: rename a field in Strapi and the mapper fails to compile immediately.