Typescript泛型的无罪推定

1,548 阅读4分钟

意指变数若未被证实及判决有罪,在审判上应推定为无罪

什么是泛型(Generic Type)?

是指在定义函式、介面或类别的时候,不预先指定具体的型别,而在使用的时候再指定型别的一种特性。

简单的说,泛型是 Typescript 的 Type Function,

可以做什么?

可以帮我们做型别推定,进去是什么,出来是什么

简单一点的例子

Select Option,有的时候我们的 value 会是 number,如果限制只能是 string ,那参数传入时需要先转换过,参数传出后也需要转换(假设API 来源与表单送出栏位都是 number),所以我们透过 Generic Type 那就可以解决这样的问题。


interface TOption<T> {
    value: T;
    text: string;
}

interface ISelect2Props<T> extends FCProps {
    value?: T
    options?: TOption<T>[]
    onChange?: (value: T) => void
}

const Select2 = <T extends string|number>({
    value,
    options,
    onChange,
}: ISelect2Props<T>) => {
    //...ignrore
}

从 1 ~ 6 回推,6就是宣告变数的概念,并且必须是 string 或 number
其中一个

可以看到我们在 onChange 的时候,typescript 已经能够推论 value 是 number

但如果我们直接设定 value type 是 string|number 的话,那onChange 则会直接就是 string|number,你要接收的那一端只会接收其中一种,就会型别错误。

复杂一点的例子

const res = rows.map(project => {
  const teamStages = project.stage?.reduce((curr: ProjectsWithGantt['children'], stage) => {
      let rowIndex = curr.findIndex(team => team.id === stage.team.id);
      const childTask = {
          id: stage.id,
          text: stage.title,
          dataLevel: EDataLevel.task,
          sequence: stage.sequence,
          links: stage.toStages?.map(row => row.toId),
      };


      if(rowIndex === -1){
          // Team
          return [...curr, {
              id: stage.team.id,
              text: stage.team.name,
              dataLevel: EDataLevel.team,
              barColor: stage.team.theme?.color,
              children: [childTask],
          }];
      }

      // Task
      curr[rowIndex].children.push(childTask);


      return curr;
  }, [])

  // Project
  return {
      id: project.id,
      text: project.name,
      dataLevel: EDataLevel.project,
      children: teamStages,
  }
})

原始将阵列物件Group写法,没有型别问题,因为没有通过一个方法包装过

需求是,我们希望把复杂的GroupBy处理,新增一个方法来包装简化使用,并且做到通用化,一般的 GroupBy 是将某个 属性当作新的 Key,而这个Group By 则是

  1. 将某个 Group By Key 的物件下了栏位也取出(id, name),
  2. 被 groupBy 的部分放在 child 下
  3. 负责处理的 Fn,需要可以取得来源物件属性
Array<{
    groupKey: string // groupBy 的 key (T)
    groupData: Custom Object // groupBy 的 key Data (D)
    child: Array<Custom Object> // 被 groupBy的資料 (C)
}>

最后方法长这样

第一个参数是需要被 groupBy 的阵列物件
第二个参数是要如何 groupBy 的方法
C 则是代表 child
D 则是代表 groupData
T 则是代表 传入 阵列物件中 的 物件

以C为范例来看,从 1 ~ 5 的回推,5则是宣告变数的概念。 3~4 的部分,就是我们把 参数从2拿到的型别,放到4的回传位置

如果你把它想成从 5 ~ 1,你就会觉得 C 需要自己带入型别,但… 麻烦了,工具是要帮助我们,不是增加繁琐工作

groupTreeBy<{
   id: string,
   repo: string,
   title: string,
   // .....ignore
}>(xxx, ()=> ...)

使用Generic TypeGroupTreeby 方法,同时回传可判别型别

推断出型别

希望的结果

bear-jsutils/src/array/array.spec.ts at main · imagine10255/bear-jsutils

Common tools and methods for project development. Contribute to imagine10255/bear-jsutils development by creating an…

github.com

你不可以做什么?

不能单纯使用变数去推断上下属性关系,意思就是你只能透过Function参数,传入A,然后让回传推论A

example

这里想要做的是,希望可以在 field 中,抓到 title 中的 key,约束避免 key 不一至的问题,没办法单纯定义一个 type 或是 interface 就直接可以推论。

无法识别出来

使用 ES6 宣告 Function 比较麻烦一点?

我们先看看使用一般方法的方式

function genericsTitleData<K extends TBodyDataFieldKey, I extends TBodyDataID>(title: TTitle<K>, data: IBodyData<K, I>[]): ITableTitleData<K, I> {
    return {title, data};
}

很好,这可以,并且也能等于相对性

接着我们把 function 改成 es6 const function

透过 webstorm 直接转

const tableTitleData = <K extends string, I extends TBodyDataID>(title: TTitle<K>, data: IBodyData<I, K>[]): ITableTitleData<K, I> => ({
    title,
    data,
});

恩,一样是OK的,也没多写什么,只不过格式有一点不一样

结论

使用 Generic Type 可以帮助我们更安全的产出代码,也更安全的型别保护。但也因为较为思考模式上较为复杂,所以还是需要审视自己实际的状况使用。

当然如果本来就觉得 TS绑手绑脚的人,可以直接略过,因为这需要一些时间去领悟(我的经验是从 JS 到 flowType 再到 Typescirpt)

medium.com/@imaginechi…