Skip to content

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:

typescript
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:

typescript
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:

typescript
type ShareLite = ProjectPayload & {
    watermark: boolean
    authorUsername: string
}

const share = (await strapi.projects.findOne(...)) as ShareLite

ShareLite 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:

typescript
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:

typescript
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.

Released under the MIT License.