第六部分:第六节 - TypeScript 与 NestJS:打造类型安全的厨房管理流程
NestJS 默认使用 TypeScript,这不仅仅是语法上的变化,更是为整个应用带来了强大的类型安全能力。想象一下,中央厨房里的每一个环节、每一个容器、每一个食材都贴上了精确的标签,并且有严格的规定(类型定义),确保不同区域之间、不同厨师之间传递的数据都是符合预期的。这大大减少了“把油当醋放”、“把盐当糖加”之类的低级错误,在编译阶段就能发现问题。
TypeScript 在 NestJS 中的优势:
- 编译时错误检查: 大量潜在的运行时错误(如属性拼写错误、参数类型不匹配)在代码编写或编译阶段就能被发现。
- 代码可读性和可维护性: 类型注解本身就是一种优秀的文档,清晰地表明了数据结构和函数签名,方便团队协作和后续维护。
- 强大的开发工具支持: VS Code 等编辑器对 TypeScript 有极好的支持,提供了智能代码补全、错误提示、重构等功能,显著提高开发效率。
使用接口 (Interface) 和类 (Class) 定义 DTOs:
DTO (Data Transfer Object) 是一个用于在应用的不同层之间(比如从控制器到服务,或者从服务到外部 API)传输数据的对象。在 NestJS 中,我们通常使用 TypeScript 的接口或类来定义 DTO 的形状,特别是用于描述请求体和响应体的数据结构。这就像为不同环节的订单或菜品定义了标准化的表格或容器。
- 接口 (Interface): 简单地描述对象的结构。轻量级,只在编译时存在。
- 类 (Class): 除了描述结构,还可以包含方法,并且在运行时存在(可以用于创建实例,也可以与 NestJS 的 Pipes 进行更复杂的验证集成)。在定义请求体 DTO 时,通常推荐使用类,因为可以结合 class-validator 和 class-transformer 库进行强大的数据验证和转换(这属于更高级的话题,这里先了解概念)。
小例子:为用户模块定义 DTO
创建一个文件 src/users/dto/create-user.dto.ts
:
// src/users/dto/create-user.dto.ts
// 使用类定义 DTO,方便后续的数据验证
export class CreateUserDto {name: string; // 必须提供 name 属性,且是 string 类型age: number; // 必须提供 age 属性,且是 number 类型
}
创建一个文件 src/users/dto/update-user.dto.ts
:
// src/users/dto/update-user.dto.ts
import { PartialType } from '@nestjs/mapped-types'; // 导入 PartialType// 使用 PartialType 基于 CreateUserDto 创建一个新类型
// PartialType 会让 CreateUserDto 中的所有属性变成可选 (?)
export class UpdateUserDto extends PartialType(CreateUserDto) {// 可以添加其他更新时特有的属性
}
PartialType
是 NestJS 提供的一个 Utility Type,它可以帮助我们方便地基于现有 DTO 创建一个所有属性都变成可选的新 DTO 类型,非常适合用于更新操作的请求体。
修改 src/users/users.controller.ts
和 src/users/users.service.ts
以使用这些 DTO:
src/users/users.controller.ts
(修改):
// src/users/users.controller.ts
import { Controller, Get, Post, Put, Delete, Param, Body, NotFoundException, HttpCode, HttpStatus } from '@nestjs/common';
import { UsersService } from './users.service';
import { ParseIntPipe } from '@nestjs/common';// 导入 DTO
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';@Controller('api/users')
export class UsersController {constructor(private readonly usersService: UsersService) {}@Get()findAll() {return this.usersService.findAll();}@Get(':id')findOne(@Param('id', ParseIntPipe) id: number) {const user = this.usersService.findOne(id);if (!user) {throw new NotFoundException(`找不到 ID 为 ${id} 的用户`);}return user;}@Post()@HttpCode(HttpStatus.CREATED)// 使用 CreateUserDto 作为请求体的类型注解create(@Body() createUserDto: CreateUserDto) {const newUser = this.usersService.create(createUserDto);return newUser; // 返回的用户对象的类型也是明确的 (User 接口)}@Put(':id')// 使用 UpdateUserDto 作为请求体的类型注解update(@Param('id', ParseIntPipe) id: number, @Body() updateUserDto: UpdateUserDto) {const updatedUser = this.usersService.update(id, updateUserDto);if (!updatedUser) {throw new NotFoundException(`找不到 ID 为 ${id} 的用户`);}return updatedUser;}@Delete(':id')@HttpCode(HttpStatus.NO_CONTENT)remove(@Param('id', ParseIntPipe) id: number) {const removed = this.usersService.remove(id);if (!removed) {throw new NotFoundException(`找不到 ID 为 ${id} 的用户,无法删除`);}}
}
src/users/users.service.ts
(修改):
// src/users/users.service.ts
import { Injectable } from '@nestjs/common';
// 导入 DTO
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';// 定义一个简单的用户接口
interface User {id: number;name: string;age: number;
}@Injectable()
export class UsersService {private users: User[] = [{ id: 1, name: 'Alice', age: 28 },{ id: 2, name: 'Bob', age: 32 },];findAll(): User[] {return this.users;}findOne(id: number): User | undefined {return this.users.find(user => user.id === id);}// create 方法的参数使用 CreateUserDto 类型create(userData: CreateUserDto): User {const newUser = {id: this.users.length > 0 ? this.users[this.users.length - 1].id + 1 : 1,...userData,};this.users.push(newUser);return newUser;}// update 方法的参数使用 UpdateUserDto (Partial<User>) 类型update(id: number, updateData: UpdateUserDto): User | undefined {const userIndex = this.users.findIndex(user => user.id === id);if (userIndex === -1) {return undefined;}this.users[userIndex] = { ...this.users[userIndex], ...updateData };return this.users[userIndex];}remove(id: number): boolean {const initialLength = this.users.length;this.users = this.users.filter(user => user.id !== id);return this.users.length < initialLength;}
}
现在,你的 NestJS 应用从请求体接收数据、到服务处理数据、再到控制器返回数据,整个流程都有明确的类型定义。这使得代码更加清晰,也更容易通过 TypeScript 编译器发现潜在的类型错误。
小结: NestJS 对 TypeScript 的原生支持是其重要优势。通过为请求/响应数据定义 DTO(使用接口或类),并为控制器和服务的方法参数和返回值添加类型注解,可以实现端到端(从网络请求到业务逻辑)的类型安全。这极大地提高了代码的健壮性和可维护性,就像中央厨房有了严格的流程和清晰的标签,确保了出品质量。
练习:
- 在你之前的
my-backend
项目中,为产品模块定义 DTO。创建一个src/products/dto
目录。 - 在
src/products/dto
目录下创建create-product.dto.ts
和update-product.dto.ts
文件。 - 在
create-product.dto.ts
中定义一个类CreateProductDto
,包含name
(string) 和price
(number) 属性。 - 在
update-product.dto.ts
中定义一个类UpdateProductDto
,使用PartialType
基于CreateProductDto
创建(或者手动定义属性为可选)。 - 修改
products.controller.ts
和products.service.ts
,在create
和update
方法中使用CreateProductDto
和UpdateProductDto
作为参数的类型注解。 - 确保你的 Service 方法返回值的类型也与你之前定义的接口或类一致(例如,
findAll
返回Product[]
)。 - 运行应用,使用 Postman 等工具测试你的产品 API,观察在发送错误类型的数据时,TypeScript 是否能在编译阶段提供帮助(例如,如果你尝试在代码中将字符串赋给 price 属性)。运行时则依赖于你是否使用了 Pipes 进行运行时验证。