使用 Express 更好地使用 TypeScript

2025-05-25

使用 Express 更好地使用 TypeScript

大家好!祝大家有美好的一天!

今天,我们将使用这两种技术进行演示Rest API。您可能认为这与Rest API其他示例没什么不同,但在本例中,我们将使用许多TypeScript高级功能,这些功能对本演示非常有帮助。但我们将更专注于TypeScript 的使用,而不是业务逻辑的实现。我建议在本例中使用 VSCode,因为它提供了许多TypeScript功能。

我们将要创建的 API 将专注于狗。我们的端点如下所示。

GET /api/v1/dogs
GET /api/v1/dogs/:id
POST /api/v1/dogs
PUT /api/v1/dogs/:id
DELETE /api/v1/dogs/:id
Enter fullscreen mode Exit fullscreen mode

狗打字

首先,创建一个文件夹,你可以随意命名。我将它命名为express-ts-api

  mkdir express-ts-api
Enter fullscreen mode Exit fullscreen mode

之后初始化一个节点项目。

 npm init --y
Enter fullscreen mode Exit fullscreen mode

我们还需要安装TypeScript

 npm i -D typescript
Enter fullscreen mode Exit fullscreen mode

我们还需要为这些ExpressNode安装类型定义。

 npm i -D @types/express @types/node
Enter fullscreen mode Exit fullscreen mode

此外,我们还将安装Express

 npm i express
Enter fullscreen mode Exit fullscreen mode

最后,将此项目配置为TypeScript项目。
使用此命令

  tsc -init
Enter fullscreen mode Exit fullscreen mode

我们的tsconfig.json将如下所示。

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true
  }
}
Enter fullscreen mode Exit fullscreen mode

简而言之,此配置告诉我们,我们的代码将以 es5 语法输出(“target”:“es5”),并且代码将基于src目录(“rootDir”:“。/src”)中的内容在目录build (“outDir”:“。/build”)中使用 CommonJS 模块系统(“module ”:“ commonjs ”) ,并且 TypeScript 语言服务应强制执行强类型检查(“strict”:“true”),最后,我们希望在不同的模块系统中导入模块,例如commonjs遵循 ES6 模块的规范(“esModuleInterop”:true),如果没有此选项,我们的导入将如下所示

import * as express from 'express';
// instead of this
// import express from 'express';
Enter fullscreen mode Exit fullscreen mode

我们的package.json将如下所示。

{
  "name": "express-ts-api",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/express": "^4.17.13",
    "@types/node": "^16.11.4",
    "typescript": "^4.4.4"
  },
  "dependencies": {
    "express": "^4.17.1"
  }
}
Enter fullscreen mode Exit fullscreen mode

我会假设你已经了解nodejs,所以我不会解释这个文件的内容。

好的,让我们开始吧。创建src目录

 mkdir src
Enter fullscreen mode Exit fullscreen mode

在src文件夹中创建app.ts文件。

import express from 'express'

const app = express()

interface Dog {
  name: string
  breed: 'labrador' | 'german shepherd' | 'golden retriever'
  adopted_at: Date | null
  birth_date: Date | null
}
Enter fullscreen mode Exit fullscreen mode

顺便说一句,我们在app对象中使用的每个 HTTP 方法都有五种泛型类型,我们可以为它们提供自定义类型。这些类型的排列顺序为ParamsResBodyReqBodyReqQueryLocals。更多关于泛型的信息 请见这里泛型帮助我们复用代码,但在我们的例子中,我们可以复用类型。

app.get<Params,ResBody,ReqBody,ReqQuery,Locals>('/api/v1/dogs',
(req,res) => { 

})
Enter fullscreen mode Exit fullscreen mode

本例中我们只使用前四种泛型类型。默认情况下,Params泛型类型的值是一个空对象。ResBodyReqBody的类型为any,而ReqQuery的类型为ParsedQs

我们将提供我们自己的类型,而不是 express 提供的默认类型。

获取/api/v1/dogs

app.get<
{},
{ data: Dog[], message: string },
{},
{ page: number, limit: number, breed: 'labrador' | 'german shepherd' | 'golden retriever' }>
('/api/v1/dogs', (req,res) => { 
  // your implementation
})
Enter fullscreen mode Exit fullscreen mode

在此端点中,我们将获取狗的列表,因此我们不会在Params泛型中传递类型,因为我们不会获取特定的狗。我们将在此端点中获取狗的列表,因此我们将其保留为空对象。其次,ResBody是我们将在res.send方法中发送的类型,在本例中,我们将发送一个对象,该对象具有两个属性:data(类型为我们之前提供的狗的数组)和message(类型为字符串,用于响应的附加信息)。第三,ReqBody是一个空对象类型,因为我们不会在此端点中接收任何数据。最后,在ReqQuery中,我们将传递一个对象类型,该对象接受分页属性pagelimit,还可以接受 breed 属性,因此我们可以使用它根据特定品种筛选狗。

获取/api/v1/dogs/:id

app.get<
{ id: number },
{ data: Dog | null, message: string },
{}>
('/api/v1/dogs/:id', (req,res) => { 
  // your implementation
})
Enter fullscreen mode Exit fullscreen mode

在这个端点中,我们将获取一只特定的狗,因此我们将在Params中传递一个对象类型,其属性为id,其类型为数字,因为我们将获取一只特定的狗。我们将在这个端点中获取狗的列表,因此我们将其保留为空对象。其次,在这种情况下, ResBody将发送一个具有两个属性data 的对象,其类型为Dog类型的联合类型null,这告诉我们,如果狗存在,它将返回Dog的形状,如果不存在,它将返回null和类型为string 的属性消息。第三,ReqBody也是一个空对象类型,因为我们不会在这个端点接收任何数据。最后,我们将为ReqQuery传递一个空对象类型,因为这个端点不需要它。

POST /api/v1/dogs

app.post<
{},
{ data: Dog & { id: number }, message: string },
Dog,
{}>
('/api/v1/dogs', (req,res) => { 
  // your implementation
})
Enter fullscreen mode Exit fullscreen mode

在这个端点中,我们将创建一只新狗,因此我们将在Params中传递一个空对象类型。其次,在本中,我们将发送一个对象,该对象具有两个属性data ,其类型是Dog类型的联合类型,以及一个对象类型,该对象具有属性id,其类型为number,因为数据库将生成此 id 而不是客户端,以及属性message,其类型为string。第三,ReqBody的类型为Dog ,因为我们将从客户端接收具有Dog形状的数据。最后,我们将为ReqQuery传递一个空对象类型,因为这个端点不需要它。

PUT /api/v1/dogs/:id

app.put<
{ id: number },
{ data: Dog & { id: number }, message: string },
Partial<Dog>,
{}>
('/api/v1/dogs', (req,res) => { 
  // your implementation
})
Enter fullscreen mode Exit fullscreen mode

在此端点中,我们将更新现有的狗,因此我们将在Params中传递一个对象类型,其属性是具有类型number的id。其次,在本中,我们将发送一个具有两个属性data的对象,其类型是Dog类型的联合类型,以及一个具有属性id的对象类型,其类型为number,因为我们将返回资源的更新值以及属性message,其类型为string。第三,ReqBody的类型为Dog ,因为我们将从具有Dog形状的客户端接收数据,但每个属性都应该是可选的,因为这是一个更新,因此我们使用 Utility Type Partial ,使Dog接口中的每个属性都成为可选的。最后,我们将为ReqQuery传递一个空对象类型,因为此端点不需要它。

删除 /api/v1/dogs/:id

app.delete<
{ id: number },
{ data: Dog & { id: number }, message: string },
{},
{}>
('/api/v1/dogs', (req,res) => { 
  // your implementation
})
Enter fullscreen mode Exit fullscreen mode

在这个端点中,我们将删除一只狗,因此我们将在Params中传递一个对象类型,其属性为id,类型为number。其次,在本中,我们将发送一个对象,该对象具有两个属性data ,其类型为Dog类型的联合类型,以及一个对象类型,该对象具有id属性,类型为number ,因为我们将返回已删除的狗资源,同时还将返回属性message,其类型为string。第三,ReqBody是一个空对象类型,因为我们不会在此端点接收任何数据。最后,我们将为ReqQuery传递一个空对象类型,因为此端点不需要它。

我想我们已经完成了。

模因完成

我觉得还没完。我们之前一直直接传递自定义类型,而有些类型在某些方法中重复出现,这导致代码不够简洁。我们来改一下。

interface BaseParams<IDType = number> {
  id: IDType
}

interface DogDetails {
  name: string
  breed: DogBreed
  adopted_at: Date | null
  birth_date: Date | null
}

interface APIResponse<Data> {
  data: Data
  message: string
}

interface Pagination {
  page: number
  limit: number
  breed: DogBreed
}

interface Empty {

}

type DogBreed = 'labrador' | 'german shepherd' | 'golden retriever'

type Dog = BaseParams & DogDetails
Enter fullscreen mode Exit fullscreen mode

好的,我将解释您看到的所有这些新类型。首先,接口BaseParams是我们将提供给Params位置的类型,BaseParams具有泛型 IDType,其默认值为类型number,您还可以通过在此处传递其他类型为id BaseParams<string> 提供不同的类型。接口DogDetails是我们将用于ReqBody位置的类型。接口APIResponse是我们将用于ResBody位置的类型,此类型也具有与BaseParams类型类似的泛型,泛型ResultType类型将是数据属性的类型。接口Pagination是我们将用于位置ReqQuery 的类型,此类型具有属性breed,它引用了我们将很快讨论的另一个自定义类型。接口Empty是一个辅助接口类型,我们将使用它来表示空对象。DogBreed类型别名也是一个辅助类型,它在Pagination接口和DogDetails接口中引用。最后,Dog类型别名是两个接口BaseParamsDogDetails的组合,我们通过使用&交集类型实现了这一点。

如果我们在代码中应用所有这些新类型,我们的代码应该是这样的。

import express from 'express'

const app = express()

interface BaseParams<IDType = number> {
  id: IDType
}

interface DogDetails {
  name: string
  breed: DogBreed
  adopted_at: Date | null
  birth_date: Date | null
}

interface APIResponse<Data> {
  data: Data
  message: string
}

interface Pagination {
  page: number
  limit: number
  breed: DogBreed
}

interface Empty {

}

type DogBreed = 'labrador' | 'german shepherd' | 'golden retriever'

type Dog = BaseParams & DogDetails

app.get<Empty, APIResponse<Dog[]>, Empty, Pagination>('/api/v1/dogs', (req, res) => {
  // your implementation
})

app.get<BaseParams, APIResponse<Dog | null>, Empty, Empty>('/api/v1/dogs/:id', (req, res) => {
  // your implementation
})

app.post<Empty, APIResponse<Dog>, DogDetails, Empty>('/api/v1/dogs', (req, res) => {
  // your implementation
})

app.put<BaseParams, APIResponse<Dog>, Partial<DogDetails>, Empty>('/api/v1/dogs', (req, res) => {
  // your implementation
})

app.delete<BaseParams, APIResponse<Dog>, Empty, Empty>('/api/v1/dogs', (req, res) => {
  // your implementation
})
Enter fullscreen mode Exit fullscreen mode

由于我们添加了新的类型,新代码比旧代码更易读、更易于维护。
我想我们真的完成了。

拉布拉多完成

感谢大家阅读这篇文章。

祝你有美好的一天😃!

文章来源:https://dev.to/macmacky/get-better-with-typescript-using-express-3ik6
PREV
我学习 Web 开发和编程的顶级 YouTube 频道祝您有美好的一天😃!。
NEXT
一篇你能理解的 Redux 简介,祝你拥有美好的一天😃!太棒了👏🏻