Skip to content

Instantly share code, notes, and snippets.

@Convly
Last active April 26, 2025 07:12
Show Gist options
  • Save Convly/6cf1e6d143bb0a90c8de2242fdedda8e to your computer and use it in GitHub Desktop.
Save Convly/6cf1e6d143bb0a90c8de2242fdedda8e to your computer and use it in GitHub Desktop.
PoC for dynamic content API response types
import type { Attribute, Common, Utils } from '@strapi/types';
type IDProperty = { id: number };
type InvalidKeys<TSchemaUID extends Common.UID.Schema> = Utils.Object.KeysBy<
Attribute.GetAll<TSchemaUID>,
Attribute.Private | Attribute.Password
>;
export type GetValues<TSchemaUID extends Common.UID.Schema> = {
[TKey in Attribute.GetOptionalKeys<TSchemaUID>]?: Attribute.Get<
TSchemaUID,
TKey
> extends infer TAttribute extends Attribute.Attribute
? GetValue<TAttribute>
: never;
} & {
[TKey in Attribute.GetRequiredKeys<TSchemaUID>]-?: Attribute.Get<
TSchemaUID,
TKey
> extends infer TAttribute extends Attribute.Attribute
? GetValue<TAttribute>
: never;
} extends infer TValues
? // Remove invalid keys (private, password)
Omit<TValues, InvalidKeys<TSchemaUID>>
: never;
type RelationValue<TAttribute extends Attribute.Attribute> = TAttribute extends Attribute.Relation<
infer _TOrigin,
infer TRelationKind,
infer TTarget
>
? Utils.Expression.MatchFirst<
[
[
Utils.Expression.Extends<TRelationKind, Attribute.RelationKind.WithTarget>,
TRelationKind extends `${string}ToMany`
? Omit<APIResponseCollection<TTarget>, 'meta'>
: APIResponse<TTarget>,
],
],
`TODO: handle other relation kind (${TRelationKind})`
>
: never;
type ComponentValue<TAttribute extends Attribute.Attribute> =
TAttribute extends Attribute.Component<infer TComponentUID, infer TRepeatable>
? Utils.Expression.If<
TRepeatable,
(IDProperty & GetValues<TComponentUID>)[],
(IDProperty & GetValues<TComponentUID>)
>
: never;
type DynamicZoneValue<TAttribute extends Attribute.Attribute> =
TAttribute extends Attribute.DynamicZone<infer TComponentUIDs>
? Array<
Utils.Array.Values<TComponentUIDs> extends infer TComponentUID
? TComponentUID extends Common.UID.Component
? { __component: TComponentUID } & IDProperty & GetValues<TComponentUID>
: never
: never
>
: never;
type MediaValue<TAttribute extends Attribute.Attribute> = TAttribute extends Attribute.Media<
infer _TKind,
infer TMultiple
>
? Utils.Expression.If<
TMultiple,
APIResponseData<'plugin::upload.file'>[],
APIResponseData<'plugin::upload.file'>
>
: never;
export type GetValue<TAttribute extends Attribute.Attribute> = Utils.Expression.If<
Utils.Expression.IsNotNever<TAttribute>,
Utils.Expression.MatchFirst<
[
// Relation
[
Utils.Expression.Extends<TAttribute, Attribute.OfType<'relation'>>,
RelationValue<TAttribute>,
],
// DynamicZone
[
Utils.Expression.Extends<TAttribute, Attribute.OfType<'dynamiczone'>>,
DynamicZoneValue<TAttribute>,
],
// Component
[
Utils.Expression.Extends<TAttribute, Attribute.OfType<'component'>>,
ComponentValue<TAttribute>,
],
// Media
[Utils.Expression.Extends<TAttribute, Attribute.OfType<'media'>>, MediaValue<TAttribute>],
// Fallback
// If none of the above attribute type, fallback to the original Attribute.GetValue (while making sure it's an attribute)
[Utils.Expression.True, Attribute.GetValue<TAttribute, unknown>],
],
unknown
>,
unknown
>;
interface APIResponseData<TContentTypeUID extends Common.UID.ContentType> extends IDProperty {
attributes: GetValues<TContentTypeUID>;
}
export interface APIResponseCollectionMetadata {
page: number;
pageSize: number;
pageCount: number;
total: number;
}
export interface APIResponse<TContentTypeUID extends Common.UID.ContentType> {
data: APIResponseData<TContentTypeUID>;
}
export interface APIResponseCollection<TContentTypeUID extends Common.UID.ContentType> {
data: APIResponseData<TContentTypeUID>[];
meta: APIResponseCollectionMetadata;
}
// TEST
declare function fetchOne<T extends Common.UID.ContentType>(uid: T): Promise<APIResponse<T>>;
declare function fetchMany<T extends Common.UID.ContentType>(
uid: T
): Promise<APIResponseCollection<T>>;
fetchOne('api::restaurant.restaurant').then((res) => {
const {
/* ... */
} = res.data.attributes;
console.log(/* ... */);
});
fetchMany('api::restaurant.restaurant').then((res) => {
res.data.forEach((entity) => {
const {
/* ... */
} = entity.attributes;
console.log(/* ... */);
});
});
@roydukkey
Copy link

Seems that IDProperty is incorrect for ComponentValue.

// https://gist.github.com/Convly/6cf1e6d143bb0a90c8de2242fdedda8e#file-strapi-content-api-ts-L47-L51

type ComponentValue<TAttribute extends Attribute.Attribute> =
  TAttribute extends Attribute.Component<infer TComponentUID, infer TRepeatable>
-    ? IDProperty &
-        Utils.Expression.If<TRepeatable, GetValues<TComponentUID>[], GetValues<TComponentUID>>
+    ? Utils.Expression.If<TRepeatable,
+      (IDProperty & GetValues<TComponentUID>)[],
+      (IDProperty & GetValues<TComponentUID>)
    : never;

@roydukkey
Copy link

Also, aren't dynamic zone suppose to be repeatable? This change fixes that.

type DynamicZoneValue<TAttribute extends Attribute.Attribute> =
  TAttribute extends Attribute.DynamicZone<infer TComponentUIDs>
    ? Array<
        Utils.Array.Values<TComponentUIDs> extends infer TComponentUID
          ? TComponentUID extends Common.UID.Component
-            ? { __component: TComponentUID } & IDProperty & GetValues<TComponentUID>
+            ? ({ __component: TComponentUID } & IDProperty & GetValues<TComponentUID>)[]
            : never
          : never
      >
    : never;

@dev-fredericfox
Copy link

@Convly care to update or comment?

@Convly
Copy link
Author

Convly commented Sep 23, 2024

@dev-fredericfox Lost track of the notifications for this gist, thanks for the ping 🙂

@roydukkey Thank you for the comments, I've updated the ComponentValue type with your suggestion. For the DynamicZone, why do you need to have Array<(...)[]> instead of just Array<(...)>? 🤔

@userlond
Copy link

userlond commented Oct 8, 2024

Does it work with Strapi V5?

@dev-fredericfox
Copy link

dev-fredericfox commented Oct 9, 2024

Does it work with Strapi V5?

@userlond No, but you don't need most of it for V5. Just import the types directly, they transitioned to TS.

@userlond
Copy link

userlond commented Oct 9, 2024

I have the admin (Strapi V4 + TS) and client project (NextJS + TS) (it's named monorepo I think?)
I try to migrate it into Strapi V5.

Till now I've used a little modifed version of this gist (with GetAttributesValues utility type).
After migrating to V5 TS compilation of client project fails because of Strapi types.

Is there any polyfill or some code example of migrating Strapi V4 -> V5 TS types (for using in client app)?

@userlond
Copy link

userlond commented Oct 9, 2024

For simplification, how to implement the test code with Strapi V5?

// TEST

declare function fetchOne<T extends Common.UID.ContentType>(uid: T): Promise<APIResponse<T>>;
declare function fetchMany<T extends Common.UID.ContentType>(
  uid: T
): Promise<APIResponseCollection<T>>;

fetchOne('api::restaurant.restaurant').then((res) => {
  const {
    /* ... */
  } = res.data.attributes;

  console.log(/* ... */);
});

fetchMany('api::restaurant.restaurant').then((res) => {
  res.data.forEach((entity) => {
    const {
      /* ... */
    } = entity.attributes;

    console.log(/* ... */);
  });
});

@dev-fredericfox
Copy link

dev-fredericfox commented Oct 9, 2024

@userlond I don't use this function, mine is much more complex, but afaik you don't need Common.UID anymore instead import UID from @strapi/types.

import type { UID } from "@strapi/types";

declare function fetchOne<T extends UID.ContentType>(uid: T): Promise<APIResponse<T>>;
declare function fetchMany<T extends UID.ContentType>(uid: T): Promise<APIResponseCollection<T>>;

@userlond
Copy link

@dev-fredericfox, thanks for reply!

@lucasassisrosa
Copy link

lucasassisrosa commented Nov 13, 2024

@userlond @dev-fredericfox for v5 when I installed @strapi/types I noticed that some imports names and locations have changed. I had to update the snippet to be

https://gist.github.com/lucasassisrosa/4168bcf144d9607b5c19c7b9af7e672c

EDIT: I had to add some overrides to make sure to remove the attributes field since v5 response data is flat

- interface APIResponseData<TContentTypeUID extends UID.ContentType> extends IDProperty {
-  attributes: GetValues<TContentTypeUID>;
- }
+ type APIResponseData<TContentTypeUID extends UID.ContentType> = IDProperty & GetValues<TContentTypeUID>;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment