
最近在學 Nest.js,練習定義請求和回應的 DTO 型別。
我在 StackBlitz 上用模擬資料實作了一個 Swagger API,整個流程是:
- Controller 接收請求
- Service 處理 MockData
- 回傳結果
結果碰到多餘參數型別不一致的問題
問題場景
stackblitz 問題重現連結
Controller 定義
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @ApiTags('users') @Controller('users') export class AppController { constructor(private readonly appService: AppService) {}
@Get() @ApiOperation({ summary: '取得全部用戶資訊', description: '返回系統中所有的用戶資訊,包括用戶名稱、角色和狀態。', }) @ApiResponse({ status: 200, description: 'User search successfully.', type: FilterUsersResponseDto, }) filterByRole( @Query() queryParams: FilterUsersRequestDto): FilterUsersResponseDto[] { return this.appService.filterMockData(queryParams); } }
|
Service 定義
1 2 3 4 5 6 7 8 9 10 11
| filterMockData(filterDto: FilterUsersRequestDto): FilterUsersResponseDto[] { const filterUserResult = this.mockData.filter((item) => { return ( (!filterDto.role || filterDto.role.includes(item.role)) && (!filterDto.status || item.status === filterDto.status) && (!filterDto.name || item.name.includes(filterDto.name)) ); });
return filterUserResult; }
|
MockData 定義
1 2 3 4 5 6 7 8 9 10 11 12 13
| private mockData = [ { id: 1, name: 'John Doe', role: UserRole.Admin, status: UserStatus.Active, email: 'john.doe@example.com', createdAt: new Date('2023-01-01'), updatedAt: new Date('2023-01-10'), test: 123, }, ];
|
乍看之下沒有什麼問題,但當我調整 MockData 新增一個值時,我發現 TS 型別檢查上並沒有報錯。
誒…..怎麼會這樣,這跟原本的型別已經不同了,多了 test 應該要報錯才對呀 ?
Excess Property Checks
官方章節
當物件以 物件字面值(Object Literal) 的形式直接傳遞時,TypeScript 會執行更嚴格的型別檢查。若物件包含目標型別未定義的屬性,編譯器會立即報錯。
1 2 3 4 5 6 7 8 9 10 11 12 13
| interface IUser { name: string, age: number }
function TestUse(user: IUser){ console.log('user', user) }
TestUse({ name: 'Antonio', age: 27, phone: 100 })
|
但如果透過變數指派的方式,TypeScript 就只會檢查有沒有符合 IUser 的屬性,多餘的屬性就不會被檢查了。
1 2 3 4 5 6 7 8 9 10 11 12 13
| interface IUser { name: string, age: number }
function TestUse(user: IUser){ console.log('user', user) }
const userObj = { name: 'Antonio', age: 27, phone: 100 }
TestUse(userObj)
|
這種繞過 Excess Property Checks 的寫法,應該只在有明確需求時才使用。在大多數情況下,出現這種情形往往代表著型別定義不夠嚴謹。
避免方式
1. 明確標註型別時會觸發檢查
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| interface IUser { name: string, age: number }
function TestUse(user: IUser){ console.log('user', user) }
const userObj: IUser = { name: 'Antonio', age: 27, phone: 100 }
TestUse(userObj)
|
2. 使用 satisfies 保留物件字面值行為
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| interface IUser { name: string, age: number }
function TestUse(user: IUser){ console.log('user', user) }
const userObj = { name: 'Antonio', age: 27, phone: 100 } satisfies IUser;
TestUse(userObj)
|
允許額外屬性的型別定義
如果業務需求確實需要允許動態新增屬性,可以考慮以下兩種方式:
1. 索引簽名(Index Signature)
1 2 3 4 5 6 7 8 9 10 11 12 13
| interface IUser { name: string, age: number, [key: string]: unknown }
function TestUse(user: IUser){ console.log('user', user) }
const userObj: IUser = { name: 'Antonio', age: 27, phone: 100 }
TestUse(userObj)
|
2. 使用介面繼承擴展原型別定義
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| interface IUser { name: string age: number }
interface IUserWithPhone extends IUser { phone: number }
function TestUse(user: IUser) { console.log('user', user) }
const userObj: IUserWithPhone = { name: 'Antonio', age: 27, phone: 100 }
TestUse(userObj)
|
3. 使用型別交集(Type Intersection)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| interface IUser { name: string age: number }
type IUserWithPhone = IUser & { phone: number }
function TestUse(user: IUser) { console.log('user', user) }
const userObj: IUserWithPhone = { name: 'Antonio', age: 27, phone: 100 }
TestUse(userObj)
|
回到問題場景的解決方案
回到文章開頭的 Nest.js 範例,只需要在定義 mockData 時明確標註型別,TypeScript 就會對多餘的屬性進行檢查並報錯:
1 2 3 4 5 6 7 8 9 10 11 12 13
| private mockData: FilterUsersResponseDto[] = [ { id: 1, name: 'John Doe', role: UserRole.Admin, status: UserStatus.Active, email: 'john.doe@example.com', createdAt: new Date('2023-01-01'), updatedAt: new Date('2023-01-10'), test: 123, }, ];
|
這樣一來,當我們不小心新增了 test 這個多餘屬性時,TypeScript 會立即提示錯誤,避免潛在的型別不一致問題。
補充資料來源