在基于 TypeScript 的项目中操作文档与条目

页面摘要: 在 Strapi v5 的 TypeScript 项目中,使用 UID 和 Data 命名空间实现类型安全地操作文档与条目,从而既能对泛型也能对已知实体类型进行带代码补全的操作。

本指南将探讨在 Strapi v5 应用中操作文档与条目的 TypeScript 模式,包括如何利用 Strapi 的 UID 和 Data 命名空间,安全地与泛型及已知实体类型进行交互。如果你正在开发基于 TypeScript 的 Strapi 项目,掌握这些方法将帮助你充分利用类型安全和代码补全,确保与应用内容和组件之间的交互稳健且无错误。

WARNING
  • Strapi 应用: 一个 Strapi v5 应用。如果你还没有,请按照文档开始。
  • TypeScript: 确保在你的 Strapi 项目中已配置 TypeScript。你可以遵循 Strapi 关于配置 TypeScript 的官方指南。
  • 已生成的类型: 应用类型已生成且可访问。 :::

类型导入

UID 命名空间包含表示应用中可用资源的字面量联合类型。

import type { UID } from '@strapi/strapi';
  • UID.ContentType 表示应用中每个内容类型标识符的联合类型
  • UID.Component 表示应用中每个组件标识符的联合类型
  • UID.Schema 表示应用中每个模式(内容类型或组件)标识符的联合类型
  • 以及其他……

Strapi 提供了一个 Data 命名空间,其中包含多个用于实体表示的內建类型。

import type { Data } from '@strapi/strapi';
  • Data.ContentType 表示一个 Strapi 文档对象
  • Data.Component 表示一个 Strapi 组件对象
  • Data.Entity 表示文档或组件之一
TIP

实体的类型定义和 UID 都基于为你的应用生成的模式类型。

如果出现不匹配或错误,你可以随时重新生成类型。

用法

泛型实体

处理泛型数据时,建议使用非参数化的 Data 类型形式。

泛型文档

async function save(name: string, document: Data.ContentType) {
  await writeCSV(name, document);
  //                    ^ {
  //                        id: Data.ID;
  //                        documentId: string;
  //                        createdAt?: DateTimeValue;
  //                        updatedAt?: DateTimeValue;
  //                        publishedAt?: DateTimeValue;
  //                        ...
  //                      }
}
WARNING

在前面的示例中,document 解析出的属性是所有内容类型共有的属性。

其他属性必须使用类型守卫手动检查。

if ('my_prop' in document) {
  return document.my_prop;
}

泛型组件

function renderComponent(parent: Node, component: Data.Component) {
  const elements: Element[] = [];
  const properties = Object.entries(component);

  for (const [name, value] of properties) {
    //        ^        ^
    //        string   any
    const paragraph = document.createElement('p');

    paragraph.textContent = `Key: ${name}, Value: ${value}`;

    elements.push(paragraph);
  }

  parent.append(...elements);
}

已知实体

操作已知实体时,可以对 Data 类型进行参数化,以获得更好的类型安全和代码补全。

已知文档

const ALL_CATEGORIES = ['food', 'tech', 'travel'];

function validateArticle(article: Data.ContentType<'api::article.article'>) {
  const { title, category } = article;
  //       ^?         ^?
  //       string     Data.ContentType<'api::category.category'>

  if (title.length < 5) {
    throw new Error('Title too short');
  }

  if (!ALL_CATEGORIES.includes(category.name)) {
    throw new Error(`Unknown category ${category.name}`);
  }
}

已知组件

function processUsageMetrics(
  id: string,
  metrics: Data.Component<'app.metrics'>
) {
  telemetry.send(id, { clicks: metrics.clicks, views: metrics.views });
}

进阶用例

实体子集

使用类型的第二个参数(TKeys),可以获取实体的一个子集。

type Credentials = Data.ContentType<'api::account.account', 'email' | 'password'>;
//   ^? { email: string; password: string }
type UsageMetrics = Data.Component<'app.metrics', 'clicks' | 'views'>;
//   ^? { clicks: number; views: number }

类型参数推断

可以基于其他函数参数来绑定并约束实体类型。

在以下示例中,uid 类型在使用时推断为 T,并用作 document 的类型参数。

import type { UID } from '@strapi/strapi';

function display<T extends UID.ContentType>(
  uid: T,
  document: Data.ContentType<T>
) {
  switch (uid) {
    case 'api::article.article': {
      return document.title;
      //              ^? string
      //     ^? Data.ContentType<'api::article.article'>
    }
    case 'api::category.category': {
      return document.name;
      //              ^? string
      //     ^? Data.ContentType<'api::category.category'>
    }
    case 'api::account.account': {
      return document.email;
      //              ^? string
      //     ^? Data.ContentType<'api::account.account'>
    }
    default: {
      throw new Error(`unknown content-type uid: "${uid}"`);
    }
  }
}

调用该函数时,document 的类型需要与给定的 uid 匹配。

declare const article: Data.Document<'api::article.article'>;
declare const category: Data.Document<'api::category.category'>;
declare const account: Data.Document<'api::account.account'>;

display('api::article.article', article);
display('api::category.category', category);
display('api::account.account', account);
// ^ ✅

display('api::article.article', category);
// ^ Error: "category" is not assignable to parameter of type ContentType<'api::article.article'>