使用 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
首先,创建一个文件夹,你可以随意命名。我将它命名为express-ts-api。
mkdir express-ts-api
之后初始化一个节点项目。
npm init --y
我们还需要安装TypeScript。
npm i -D typescript
我们还需要为这些Express和Node安装类型定义。
npm i -D @types/express @types/node
此外,我们还将安装Express
npm i express
最后,将此项目配置为TypeScript项目。
使用此命令
tsc -init
我们的tsconfig.json将如下所示。
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true
}
}
简而言之,此配置告诉我们,我们的代码将以 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';
我们的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"
}
}
我会假设你已经了解nodejs,所以我不会解释这个文件的内容。
好的,让我们开始吧。创建src目录
mkdir src
在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
}
顺便说一句,我们在app对象中使用的每个 HTTP 方法都有五种泛型类型,我们可以为它们提供自定义类型。这些类型的排列顺序为Params、ResBody、ReqBody、ReqQuery和Locals。更多关于泛型的信息 请见这里。泛型帮助我们复用代码,但在我们的例子中,我们可以复用类型。
app.get<Params,ResBody,ReqBody,ReqQuery,Locals>('/api/v1/dogs',
(req,res) => {
})
本例中我们只使用前四种泛型类型。默认情况下,Params泛型类型的值是一个空对象。ResBody和ReqBody的类型为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
})
在此端点中,我们将获取狗的列表,因此我们不会在Params泛型中传递类型,因为我们不会获取特定的狗。我们将在此端点中获取狗的列表,因此我们将其保留为空对象。其次,ResBody是我们将在res.send方法中发送的类型,在本例中,我们将发送一个对象,该对象具有两个属性:data(类型为我们之前提供的狗的数组)和message(类型为字符串,用于响应的附加信息)。第三,ReqBody是一个空对象类型,因为我们不会在此端点中接收任何数据。最后,在ReqQuery中,我们将传递一个对象类型,该对象接受分页属性page和limit,还可以接受 breed 属性,因此我们可以使用它根据特定品种筛选狗。
获取/api/v1/dogs/:id
app.get<
{ id: number },
{ data: Dog | null, message: string },
{}>
('/api/v1/dogs/:id', (req,res) => {
// your implementation
})
在这个端点中,我们将获取一只特定的狗,因此我们将在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
})
在这个端点中,我们将创建一只新狗,因此我们将在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
})
在此端点中,我们将更新现有的狗,因此我们将在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
})
在这个端点中,我们将删除一只狗,因此我们将在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
好的,我将解释您看到的所有这些新类型。首先,接口BaseParams是我们将提供给Params位置的类型,BaseParams具有泛型 IDType,其默认值为类型number,您还可以通过在此处传递其他类型为id BaseParams<string>
提供不同的类型。接口DogDetails是我们将用于ReqBody位置的类型。接口APIResponse是我们将用于ResBody位置的类型,此类型也具有与BaseParams类型类似的泛型,泛型ResultType类型将是数据属性的类型。接口Pagination是我们将用于位置ReqQuery 的类型,此类型具有属性breed,它引用了我们将很快讨论的另一个自定义类型。接口Empty是一个辅助接口类型,我们将使用它来表示空对象。DogBreed类型别名也是一个辅助类型,它在Pagination接口和DogDetails接口中引用。最后,Dog类型别名是两个接口BaseParams和DogDetails的组合,我们通过使用&交集类型实现了这一点。
如果我们在代码中应用所有这些新类型,我们的代码应该是这样的。
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
})
由于我们添加了新的类型,新代码比旧代码更易读、更易于维护。
我想我们真的完成了。