不要這樣使用 TypeScript 類型。改用 Map Pattern 吧。

,

介紹

在處理一個實際專案時,我遇到了一個特定的 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),透過擴展來新增功能,而無需修改現有程式碼。

使用這種模式,我們可以輕鬆擴展反應列表,而無需修改太多文件,同時仍然嚴格控制可以添加的內容。