介紹
在處理一個實際專案時,我遇到了一個特定的 TypeScript 實現,它雖然功能正常,但缺乏靈活性。在這篇部落格中,我將帶你了解我遇到的問題,以及如何通過使用Map 模式來改進設計,使其更具動態性。
這個問題
我遇到了這個 TypeScript 類型:
// FinalResponse.ts
import { Reaction } from './Reaction'
export type FinalResponse = {
totalScore: number
headingsPenalty: number
sentencesPenalty: number
charactersPenalty: number
wordsPenalty: number
headings: string[]
sentences: string[]
words: string[]
links: { href: string; text: string }[]
exceeded: {
exceededSentences: string[]
repeatedWords: { word: string; count: number }[]
}
reactions: {
likes: Reaction
unicorns: Reaction
explodingHeads: Reaction
raisedHands: Reaction
fire: Reaction
}
}
此外,這個 Reaction 類型已被定義:
// Reaction.ts
export type Reaction = {
count: number
percentage: number
}
而這在一個函數中是這樣使用的:
// calculator.ts
export const calculateScore = (
headings: string[],
sentences: string[],
words: string[],
totalPostCharactersCount: number,
links: { href: string; text: string }[],
reactions: {
likes: Reaction
unicorns: Reaction
explodingHeads: Reaction
raisedHands: Reaction
fire: Reaction
},
): FinalResponse => {
// Score calculation logic...
}
這種方法的問題
現在,想像一下開發者需要添加新反應(例如,愛心、鼓掌等)的情境。
鑑於目前的設置,他們必須:
- 修改
FinalResponse.ts檔案以新增反應類型。 - 如有必要,請更新
Reaction.ts類型。 - 修改
calculateScore函數以包含新的反應。 - 可能需要更新依賴此結構的應用程式的其他部分。
因此,他們不是在一個地方添加新的反應,而是最終在三個或更多文件中進行更改,這增加了錯誤和冗餘的可能性。這種方法是緊密耦合的。
解決方案
我提出了一個更乾淨的解決方案,通過引入一個更靈活且可重用的結構。
// FinalResponse.ts
import { Reaction } from './Reaction'
export type ReactionMap = Record<string, Reaction>
export type FinalResponse = {
totalScore: number
headingsPenalty: number
sentencesPenalty: number
charactersPenalty: number
wordsPenalty: number
headings: string[]
sentences: string[]
words: string[]
links: { href: string; text: string }[]
exceeded: {
exceededSentences: string[]
repeatedWords: { word: string; count: number }[]
}
reactions: ReactionMap
}
解釋:
ReactionMap: 這個類型使用了Record<string, Reaction>,這意味著任何字串都可以作為鍵,而值將始終是Reaction類型。FinalResponse: 現在,FinalResponse中的 reactions 欄位是ReactionMap類型,讓你可以動態添加任何反應,而無需修改多個文件。
乾淨的程式碼
在 calculator.ts 文件中,函數現在看起來像這樣:
// calculator.ts
export const calculateScore = (
headings: string[],
sentences: string[],
words: string[],
totalPostCharactersCount: number,
links: { href: string; text: string }[],
reactions: ReactionMap,
): FinalResponse => {
// Score calculation logic...
}
但等等!我們需要一些控制
雖然新的解決方案提供了靈活性,但它也帶來了添加未經檢查反應的風險,這意味著任何人都可能將任何字串作為反應添加。我們絕對不希望這樣。
為了解決這個問題,我們可以對允許的反應實施更嚴格的控制。
更安全的解決方案
這是更新後的版本,我們將反應限制在預先定義的一組允許值中:
// FinalResponse.ts
import { Reaction } from './Reaction'
type AllowedReactions =
| 'likes'
| 'unicorns'
| 'explodingHeads'
| 'raisedHands'
| 'fire'
export type ReactionMap = {
[key in AllowedReactions]: Reaction
}
export type FinalResponse = {
totalScore: number
headingsPenalty: number
sentencesPenalty: number
charactersPenalty: number
wordsPenalty: number
headings: string[]
sentences: string[]
words: string[]
links: { href: string; text: string }[]
exceeded: {
exceededSentences: string[]
repeatedWords: { word: string; count: number }[]
}
reactions: ReactionMap
}
視覺化呈現


結論
這種方法在靈活性與控制之間取得了平衡:
- 靈活性:你可以通過僅修改
AllowedReactions類型來輕鬆添加新的反應。 - 控制: 使用聯合類型確保只能使用允許的反應,防止添加無效或不需要的反應的風險。
這段程式碼遵循了開放/封閉原則 (OCP),透過擴展來新增功能,而無需修改現有程式碼。
使用這種模式,我們可以輕鬆擴展反應列表,而無需修改太多文件,同時仍然嚴格控制可以添加的內容。