typescript - 如何使用接口(interface)使 API 响应类型安全/声明? typescript

标签 typescript validation jsonschema contentful

我在同一个项目中使用 TypeScript 和 Contentful,但我在思考如何以干净、合理和动态的方式在界面中映射 API 响应时遇到问题,因为来自 contentful 的 API 响应是巨大且丑陋 - 对于每种不同的内容类型,它们可能有所不同。

所以这些是我的主要问题:

  1. 实际上是在界面中映射 API 响应 - 它太大了,而且有很多不必要的数据,我需要将其全部放入界面中吗?
  2. 如果客户端稍后创建新的内容类型,那么接下来的 api 响应的结构会略有不同,此时接口(interface)会出现错误,我该怎么办?

感谢所有帮助,实际上只需要以正确的方式研究解决方案的建议。

这是内容丰富的 API 响应之一的示例:

{
   "sys":{
      "type":"Array"
   },
   "total":2,
   "skip":0,
   "limit":100,
   "items":[
      {
         "metadata":{
            "tags":[
               
            ]
         },
         "sys":{
            "space":{
               "sys":{
                  "type":"Link",
                  "linkType":"Space",
                  "id":"rikydtrxnc79"
               }
            },
            "id":"1w9AMDdCqRHRMEfzif3J0V",
            "type":"Entry",
            "createdAt":"2023-06-22T14:17:28.969Z",
            "updatedAt":"2023-06-22T14:17:28.969Z",
            "environment":{
               "sys":{
                  "id":"master",
                  "type":"Link",
                  "linkType":"Environment"
               }
            },
            "revision":1,
            "contentType":{
               "sys":{
                  "type":"Link",
                  "linkType":"ContentType",
                  "id":"blogPost"
               }
            },
            "locale":"en-US"
         },
         "fields":{
            "title":"Second Test",
            "body":{
               "data":{
                  
               },
               "content":[
                  {
                     "data":{
                        
                     },
                     "content":[
                        {
                           "data":{
                              
                           },
                           "marks":[
                              
                           ],
                           "value":"This is a test.",
                           "nodeType":"text"
                        }
                     ],
                     "nodeType":"paragraph"
                  }
               ],
               "nodeType":"document"
            },
            "tags":[
               {
                  "sys":{
                     "type":"Link",
                     "linkType":"Entry",
                     "id":"5vuJaOnrVvh34oAiBt8qgh"
                  }
               },
               {
                  "sys":{
                     "type":"Link",
                     "linkType":"Entry",
                     "id":"6feqnd9taR55ruluGBsp8h"
                  }
               }
            ]
         }
      },
      {
         "metadata":{
            "tags":[
               
            ]
         },
         "sys":{
            "space":{
               "sys":{
                  "type":"Link",
                  "linkType":"Space",
                  "id":"rikydtrxnc79"
               }
            },
            "id":"32eWu0P7zXgbxqum5nbdqP",
            "type":"Entry",
            "createdAt":"2023-06-22T14:14:34.053Z",
            "updatedAt":"2023-06-22T14:14:34.053Z",
            "environment":{
               "sys":{
                  "id":"master",
                  "type":"Link",
                  "linkType":"Environment"
               }
            },
            "revision":1,
            "contentType":{
               "sys":{
                  "type":"Link",
                  "linkType":"ContentType",
                  "id":"blogPost"
               }
            },
            "locale":"en-US"
         },
         "fields":{
            "title":"Test",
            "body":{
               "data":{
                  
               },
               "content":[
                  {
                     "data":{
                        
                     },
                     "content":[
                        {
                           "data":{
                              
                           },
                           "marks":[
                              
                           ],
                           "value":"This is a test",
                           "nodeType":"text"
                        }
                     ],
                     "nodeType":"heading-1"
                  },
                  {
                     "data":{
                        
                     },
                     "content":[
                        {
                           "data":{
                              
                           },
                           "marks":[
                              
                           ],
                           "value":"",
                           "nodeType":"text"
                        }
                     ],
                     "nodeType":"paragraph"
                  },
                  {
                     "data":{
                        
                     },
                     "content":[
                        {
                           "data":{
                              
                           },
                           "marks":[
                              
                           ],
                           "value":"This is a test",
                           "nodeType":"text"
                        }
                     ],
                     "nodeType":"heading-3"
                  },
                  {
                     "data":{
                        
                     },
                     "content":[
                        {
                           "data":{
                              
                           },
                           "marks":[
                              
                           ],
                           "value":"",
                           "nodeType":"text"
                        }
                     ],
                     "nodeType":"paragraph"
                  },
                  {
                     "data":{
                        
                     },
                     "content":[
                        {
                           "data":{
                              
                           },
                           "marks":[
                              
                           ],
                           "value":"This is a test.",
                           "nodeType":"text"
                        }
                     ],
                     "nodeType":"paragraph"
                  },
                  {
                     "data":{
                        
                     },
                     "content":[
                        {
                           "data":{
                              
                           },
                           "marks":[
                              
                           ],
                           "value":"",
                           "nodeType":"text"
                        }
                     ],
                     "nodeType":"paragraph"
                  }
               ],
               "nodeType":"document"
            },
            "tags":[
               {
                  "sys":{
                     "type":"Link",
                     "linkType":"Entry",
                     "id":"63QnQZsSKUUI3TNAvsI19J"
                  }
               },
               {
                  "sys":{
                     "type":"Link",
                     "linkType":"Entry",
                     "id":"1O7OQ1YFLMshW7BwBTL3ER"
                  }
               }
            ],
            "images":[
               {
                  "sys":{
                     "type":"Link",
                     "linkType":"Asset",
                     "id":"35LgxsKv96DyW51Uu8DrnM"
                  }
               },
               {
                  "sys":{
                     "type":"Link",
                     "linkType":"Asset",
                     "id":"4bxRR1bBIknnVzRjeqvUIr"
                  }
               }
            ]
         }
      }
   ],
   "includes":{
      "Entry":[
         {
            "metadata":{
               "tags":[
                  
               ]
            },
            "sys":{
               "space":{
                  "sys":{
                     "type":"Link",
                     "linkType":"Space",
                     "id":"rikydtrxnc79"
                  }
               },
               "id":"1O7OQ1YFLMshW7BwBTL3ER",
               "type":"Entry",
               "createdAt":"2023-06-22T14:13:23.918Z",
               "updatedAt":"2023-06-22T14:13:23.918Z",
               "environment":{
                  "sys":{
                     "id":"master",
                     "type":"Link",
                     "linkType":"Environment"
                  }
               },
               "revision":1,
               "contentType":{
                  "sys":{
                     "type":"Link",
                     "linkType":"ContentType",
                     "id":"countryTag"
                  }
               },
               "locale":"en-US"
            },
            "fields":{
               "countryTag":"usa"
            }
         },
         {
            "metadata":{
               "tags":[
                  
               ]
            },
            "sys":{
               "space":{
                  "sys":{
                     "type":"Link",
                     "linkType":"Space",
                     "id":"rikydtrxnc79"
                  }
               },
               "id":"5vuJaOnrVvh34oAiBt8qgh",
               "type":"Entry",
               "createdAt":"2023-06-22T10:50:36.561Z",
               "updatedAt":"2023-06-22T10:50:36.561Z",
               "environment":{
                  "sys":{
                     "id":"master",
                     "type":"Link",
                     "linkType":"Environment"
                  }
               },
               "revision":1,
               "contentType":{
                  "sys":{
                     "type":"Link",
                     "linkType":"ContentType",
                     "id":"countryTag"
                  }
               },
               "locale":"en-US"
            },
            "fields":{
               "countryTag":"australia"
            }
         },
         {
            "metadata":{
               "tags":[
                  
               ]
            },
            "sys":{
               "space":{
                  "sys":{
                     "type":"Link",
                     "linkType":"Space",
                     "id":"rikydtrxnc79"
                  }
               },
               "id":"63QnQZsSKUUI3TNAvsI19J",
               "type":"Entry",
               "createdAt":"2023-06-22T14:13:11.295Z",
               "updatedAt":"2023-06-22T14:13:11.295Z",
               "environment":{
                  "sys":{
                     "id":"master",
                     "type":"Link",
                     "linkType":"Environment"
                  }
               },
               "revision":1,
               "contentType":{
                  "sys":{
                     "type":"Link",
                     "linkType":"ContentType",
                     "id":"regionTag"
                  }
               },
               "locale":"en-US"
            },
            "fields":{
               "title":"americas"
            }
         },
         {
            "metadata":{
               "tags":[
                  
               ]
            },
            "sys":{
               "space":{
                  "sys":{
                     "type":"Link",
                     "linkType":"Space",
                     "id":"rikydtrxnc79"
                  }
               },
               "id":"6feqnd9taR55ruluGBsp8h",
               "type":"Entry",
               "createdAt":"2023-06-22T10:41:10.693Z",
               "updatedAt":"2023-06-22T10:41:10.693Z",
               "environment":{
                  "sys":{
                     "id":"master",
                     "type":"Link",
                     "linkType":"Environment"
                  }
               },
               "revision":1,
               "contentType":{
                  "sys":{
                     "type":"Link",
                     "linkType":"ContentType",
                     "id":"regionTag"
                  }
               },
               "locale":"en-US"
            },
            "fields":{
               "title":"apac"
            }
         }
      ],
      "Asset":[
         {
            "metadata":{
               "tags":[
                  
               ]
            },
            "sys":{
               "space":{
                  "sys":{
                     "type":"Link",
                     "linkType":"Space",
                     "id":"rikydtrxnc79"
                  }
               },
               "id":"35LgxsKv96DyW51Uu8DrnM",
               "type":"Asset",
               "createdAt":"2023-06-22T14:14:11.979Z",
               "updatedAt":"2023-06-22T14:14:11.979Z",
               "environment":{
                  "sys":{
                     "id":"master",
                     "type":"Link",
                     "linkType":"Environment"
                  }
               },
               "revision":1,
               "locale":"en-US"
            },
            "fields":{
               "title":"Test1",
               "description":"",
               "file":{
                  "url":"//images.ctfassets.net/rikydtrxnc79/35LgxsKv96DyW51Uu8DrnM/d8a11b6dc57a416a143af10b3569a7e0/pexels-photo-3844788.jpeg",
                  "details":{
                     "size":83381,
                     "image":{
                        "width":500,
                        "height":667
                     }
                  },
                  "fileName":"pexels-photo-3844788.jpeg",
                  "contentType":"image/jpeg"
               }
            }
         },
         {
            "metadata":{
               "tags":[
                  
               ]
            },
            "sys":{
               "space":{
                  "sys":{
                     "type":"Link",
                     "linkType":"Space",
                     "id":"rikydtrxnc79"
                  }
               },
               "id":"4bxRR1bBIknnVzRjeqvUIr",
               "type":"Asset",
               "createdAt":"2023-06-22T14:14:26.979Z",
               "updatedAt":"2023-06-22T14:14:26.979Z",
               "environment":{
                  "sys":{
                     "id":"master",
                     "type":"Link",
                     "linkType":"Environment"
                  }
               },
               "revision":1,
               "locale":"en-US"
            },
            "fields":{
               "title":"Test2",
               "description":"",
               "file":{
                  "url":"//images.ctfassets.net/rikydtrxnc79/4bxRR1bBIknnVzRjeqvUIr/4ffca3cdb4db84b6ee2129cf05f06cdc/premium_photo-1661964088064-dd92eaaa7dcf.jpeg",
                  "details":{
                     "size":46936,
                     "image":{
                        "width":1000,
                        "height":714
                     }
                  },
                  "fileName":"premium_photo-1661964088064-dd92eaaa7dcf.jpeg",
                  "contentType":"image/jpeg"
               }
            }
         }
      ]
   }
}

现在,项目数组和包含数组在对象中可能有不同的数据集,因此这就是为什么我发现很难思考如何解决新内容类型和现有接口(interface)/模式不适用于它们的问题.

我已经制定了这样的响应,但认为它太过分了?

export interface ContentfulApiResponse {
  sys: {
    type: string;
  };
  total: number;
  skip: number;
  limit: number;
  items: [
    {
      metadata: {
        tags: [];
      };
      sys: {
        space: {
          sys: {
            type: string;
            linkType: string;
            id: string;
          };
        };
        id: string;
        type: string;
        createdAt: string;
        updatedAt: string;
        environment: {
          sys: {
            id: string;
            type: string;
            linkType: string;
          };
        };
        revision: number;
        contentType: {
          sys: {
            type: string;
            linkType: string;
            id: string;
          };
        };
        locale: string;
      };
      fields: {
        title: string;
        body: {
          data: {};
          content: [
            {
              data: {};
              content: [
                {
                  data: {};
                  marks: [];
                  value: string;
                  nodeType: string;
                }
              ];
              nodeType: string;
            }
          ];
          nodeType: string;
        };
        tags: [
          {
            sys: {
              type: string;
              linkType: string;
              id: string;
            };
          },
          {
            sys: {
              type: string;
              linkType: string;
              id: string;
            };
          }
        ];
      };
    },
    {
      metadata: {
        tags: [];
      };
      sys: {
        space: {
          sys: {
            type: string;
            linkType: string;
            id: string;
          };
        };
        id: string;
        type: string;
        createdAt: string;
        updatedAt: string;
        environment: {
          sys: {
            id: string;
            type: string;
            linkType: string;
          };
        };
        revision: number;
        contentType: {
          sys: {
            type: string;
            linkType: string;
            id: string;
          };
        };
        locale: string;
      };
      fields: {
        title: string;
        body: {
          data: {};
          content: [
            {
              data: {};
              content: [
                {
                  data: {};
                  marks: [];
                  value: string;
                  nodeType: string;
                }
              ];
              nodeType: string;
            },
            {
              data: {};
              content: [
                {
                  data: {};
                  marks: [];
                  value: string;
                  nodeType: string;
                }
              ];
              nodeType: string;
            },
            {
              data: {};
              content: [
                {
                  data: {};
                  marks: [];
                  value: string;
                  nodeType: string;
                }
              ];
              nodeType: string;
            },
            {
              data: {};
              content: [
                {
                  data: {};
                  marks: [];
                  value: "";
                  nodeType: string;
                }
              ];
              nodeType: string;
            },
            {
              data: {};
              content: [
                {
                  data: {};
                  marks: [];
                  value: string;
                  nodeType: string;
                }
              ];
              nodeType: string;
            },
            {
              data: {};
              content: [
                {
                  data: {};
                  marks: [];
                  value: string;
                  nodeType: string;
                }
              ];
              nodeType: string;
            }
          ];
          nodeType: string;
        };
        tags: [
          {
            sys: {
              type: string;
              linkType: string;
              id: string;
            };
          },
          {
            sys: {
              type: string;
              linkType: string;
              id: string;
            };
          }
        ];
        images: [
          {
            sys: {
              type: string;
              linkType: string;
              id: string;
            };
          },
          {
            sys: {
              type: string;
              linkType: string;
              id: string;
            };
          }
        ];
      };
    }
  ];
  includes: {
    Entry: ContentfulIncludesEntry[];
    Asset: ContentfulIncludesAsset[];
  };
}

interface ContentfulIncludesAsset {
  metadata: {
    tags: [];
  };
  sys: {
    space: {
      sys: {
        type: string;
        linkType: string;
        id: string;
      };
    };
    id: string;
    type: string;
    createdAt: string;
    updatedAt: string;
    environment: {
      sys: {
        id: string;
        type: string;
        linkType: string;
      };
    };
    revision: number;
    locale: string;
  };
  fields: {
    title: string;
    description: string;
    file: {
      url: string;
      details: {
        size: number;
        image: {
          width: number;
          height: number;
        };
      };
      fileName: string;
      contentType: string;
    };
  };
}

interface ContentfulIncludesEntry {
  metadata: {
    tags: [];
  };
  sys: {
    space: {
      sys: {
        type: string;
        linkType: string;
        id: string;
      };
    };
    id: string;
    type: string;
    createdAt: string;
    updatedAt: string;
    environment: {
      sys: {
        id: string;
        type: string;
        linkType: string;
      };
    };
    revision: number;
    contentType: {
      sys: {
        type: string;
        linkType: string;
        id: string;
      };
    };
    locale: string;
  };
  fields: {
    countryTag?: string;
    regionTag?: string;
  };
}

请记住,字段对象可能会随每种内容类型而变化。

最佳答案

我的第一个想法是,您应该首先为每个属性名称创建一个通用类型。系统、元数据、标签、环境等,完全忽略它们将具有循环引用的事实,这可能会阻止它们编译,因为在这个阶段您只想将其全部记录下来并理解它。例如:

interface Item {
  metadata?: Metadata;
  sys?: Sys;
  fields?: Fields;
}

另一个想法是甚至不要尝试强类型化服务器响应。相反,接受响应未知,然后使用 zod将数据断言为您的代码期望的类型。这样做的一个优点是,运行时断言将帮助您以单元测试和静态类型检查无法做到的方式发现错误和意外。

但是,您确定需要自己执行这些操作吗?如果类似 @bgschiller/contentful-typescript-codegen没有帮助,我希望你能在那里找到你可以使用的东西

(注意:我不推荐 @bgschiller/contentful-typescript-codegen。我以前从未听说过它,只是通过在 npmjs.com 上搜索“types/contentful”找到它。)

关于typescript - 如何使用接口(interface)使 API 响应类型安全/声明? typescript ,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/76550838/

相关文章:

typescript - 如何扩展 keyof 类型以使其包含 key 的修改版本,例如以 '-' 为前缀?

javascript - 使用 JavaScript 的 Selenium Webdriver,如何使用 chromedriver.exe 的特定路径启动 Chrome?

html - 如何在 Angular 5 中防止 html 注入(inject)

validation - Angular 2 : Conditional required validation

validation - JSR 303 中的 GroupSequence 和有序评估

树结构的 JSON 模式

angular - 类型错误 : Cannot read property 'http' of undefined angular 2

.net - 选择哪个验证框架 : Spring Validation or Validation Application Block (Enterprise LIbrary 4. 0)?

JSON 模式语义注释

JSON 架构生成器程序