使用 Partial 设置可变的默认参数

当一个函数中有默认参数的时候,若此参数是一个对象,那么我们需要对这个对象的每一个field 都设置类型。而且当新增 field 的时候,还需要在多个地方进行新增。

  1. type Options = {
  2. from: string;
  3. to: string;
  4. };
  5. const defaultOptions: Options = {
  6. from: "./src",
  7. to: "./dest",
  8. };
  9. type PartialOptions = {
  10. from?: string;
  11. to?: string;
  12. };
  13. function copy(options: PartialOptions) {
  14. const allOptions = { ...defaultOptions, ...options};
  15. // todo: do something
  16. }

遇到这种情况的时候,我们可以使用 Partial 来进行精简:

  1. const defaultOptions = {
  2. from: "./src",
  3. to: "./dest",
  4. overwrite: true,
  5. };
  6. function copy(options: Partial<typeof defaultOptions>) {
  7. const allOptions = { ...defaultOptions, ...options};
  8. // todo: do something
  9. }

这样,就可以在保证类型安全的前提下进行随意的扩展。这时我们只需要关注唯一的一个数据源 defaultOptions,这是唯一实际出现在运行时的对象。这样书写能减轻 TS 的侵入性。

Tuple to union

在 const 上下文、typeof 操作符以及索引存取符的作用下,可以将 tuple 转化成 union。

  1. const categories = [
  2. "beginner",
  3. "intermediate",
  4. "advanced",
  5. ] as const;
  6. // "beginner" | "intermediate" | "advanced"
  7. type Category = (typeof categories)[number]

同样的,这时候我们只需要维护一份数据,可以在 categories 中进行增减操作。

用类型进行逻辑操作

假设我们有多种游戏类型,每种类型都有一个 kind field 来标识游戏类型,我们可以得到以下的表示方式:

  1. // 类型公共的 field
  2. type ToyBase = {
  3. name: string;
  4. price: number;
  5. quantity: number;
  6. minimumAge: number;
  7. }
  8. // 具体的类型
  9. type BoardGame = ToyBase & {
  10. kind: 'boardgame';
  11. players: number;
  12. }
  13. type Puzzle = ToyBase & {
  14. kind: 'puzzle';
  15. pieces: number;
  16. }
  17. type Doll = ToyBase & {
  18. kind: 'doll';
  19. material: 'plastic' | 'plush'
  20. }
  21. // 玩具的类型
  22. type Toy = BoardGame | Puzzle | Doll

我们可以借助 kind 属性来进行类型收窄判断

  1. function printToy(toy: Toy) {
  2. switch(toy.kind) {
  3. case "boardgame":
  4. // todo
  5. break;
  6. case "puzzle":
  7. // todo
  8. break;
  9. case "doll":
  10. // todo
  11. break;
  12. default:
  13. console.log(toy);
  14. }
  15. }

如果需要更为详尽的类型分类,我们会有以下新的类型:

  1. type ToyKind = 'boardgame' | 'puzzle' | 'doll'
  2. type GroupedToys = {
  3. boardgame: BoardGame[];
  4. puzzle: Puzzle[];
  5. doll: Doll[];
  6. }

这时候,如果我们想新增一个游戏类型,比如说 VideoGame:

  1. type VideoGame = ToyBase & {
  2. kind: 'videogame';
  3. system: 'Switch' | 'Wii' | 'PS5'
  4. }

这时候,我们需要同时对 ToyToyKindGroupedToys 来进行修改。这时候我们可以使用 TS 内置的函数来降低维护成本。

  1. // ToyKind 可以使用另一种表示方式
  2. type ToyKind = Toy['kind'];

下一步,我们可以构造一个类型推导函数来对具体类型进行推导:

  1. type GetKind<Group, Kind> = Extract<Group, { kind: Kind }>
  2. // 这时,GroupedToys 类型就可以自行进行推导
  3. type GroupedToys = {
  4. [Kind in ToyKind]: GetKind<Toy, Kind>[]
  5. }
  6. // 上面的写法就等效于
  7. type GroupedToys = {
  8. boardgame: BoardGame[];
  9. puzzle: Puzzle[];
  10. doll: Doll[];
  11. }

我们也可以使用 as 操作符来对属性进行修改:

  1. type GroupedToys = {
  2. [Kind in ToyKind as `${Kind}s`]: GetKind<Toy, Kind>[]
  3. };
  4. // 等效于
  5. type GroupedToys = {
  6. boardgames: BoardGame[];
  7. puzzles: Puzzle[];
  8. dolls: Doll[];
  9. }

总结

通过上面的例子,我们可以总结出 ToyKindGroupedToys都属于衍生类型,对于衍生类型我们需要尽可能的使用推导来产生,这样就可以减少我们需要维护的类型数量。通常,以下的几个要点可以帮助我们降低类型维护的复杂度:

  1. 使用模型来创建数据或者从已有类型中进行推导
  2. 定义衍生类型(mapped types 或者 Partials)
  3. 定义行为(条件判断)