不要使用这样的 TypeScript 类型。请使用 Map 模式

2025-05-27

不要使用这样的 TypeScript 类型。请使用 Map 模式

介绍

在实际项目中,我遇到了一个 TypeScript 实现,它虽然功能齐全,但缺乏灵活性。在这篇博客中,我将带您了解我遇到的问题,以及如何通过使用Map 模式实现更动态的方法来改进设计。

目录

  1. 问题
  2. 这种方法的问题
  3. 解决方案
  4. 干净的代码
  5. 更安全的解决方案
  6. 视觉表现
  7. 结论

问题

我遇到了这种 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
  }
}
Enter fullscreen mode Exit fullscreen mode

此外,该Reaction类型定义如下:

// Reaction.ts
export type Reaction = {
  count: number
  percentage: number
}
Enter fullscreen mode Exit fullscreen mode

这在如下函数中使用:

// 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...
}
Enter fullscreen mode Exit fullscreen mode

这种方法的问题

现在,想象一下开发者需要添加新的反应(例如,心动、鼓掌等)的场景。
在当前设置下,他们需要:

  • 修改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
}
Enter fullscreen mode Exit fullscreen mode

解释:

  • ReactionMap:此类型使用Record<string, Reaction>,这意味着任何字符串都可以作为键,并且值始终是类型Reaction
  • FinalResponse:现在,中的反应字段FinalResponse是类型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...
}
Enter fullscreen mode Exit fullscreen mode

可是等等!我们需要一些控制

虽然新的解决方案提供了灵活性,但也带来了添加未经检查的反应的风险,这意味着任何人都可能将任意字符串添加为反应。我们绝对不希望出现这种情况。

为了解决这个问题,我们可以对允许的反应实施更严格的控制。

更安全的解决方案

这是更新后的版本,我们将反应限制为一组预定义的允许值:

// 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
}
Enter fullscreen mode Exit fullscreen mode

视觉表现

TypeScript 类型

TypeScript 类型

结论

这种方法在灵活性和控制力之间取得了平衡:

  • 灵活性:您只需修改AllowedReactions类型即可轻松添加新的反应。
  • 控制:使用联合类型可确保只能使用允许的反应,从而防止添加无效或不需要的反应的风险。

该代码遵循开放/封闭原则 (OCP),可以通过扩展添加新功能,而无需修改现有代码。

通过这种模式,我们可以轻松扩展反应列表而无需修改太多文件,同时仍然对可以添加的内容保持严格的控制。

代码?

您可以在此处访问存储库


希望这个解决方案对你有帮助!感谢阅读。😊

在GitHub上关注我


文章来源:https://dev.to/perisicnikola37/dont-use-typescript-types-like-this-use-map-pattern-instead-ki3
PREV
新的 CSS 媒体查询语法💥
NEXT
30 年软件工程师职业生涯的 30 条建议