翻看了项目中 250 个 Typescript 类型定义文件,我发现了这些操作

1,482 阅读5分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

Typescript 现在已经很普及了,在这里我列出一些写 Typescript 会遇到的难点场景,以及对应的解决办法,希望能够帮助到那些想要写好 Typescript 类型的小伙伴~

下面的场景均为这一段时间来,在实际需求中遇到、在历史代码中见到的场景,有些可能会比较简单,但是出现的次数比较多,有些是因为个人觉得比较难。
也欢迎大家在评论区补充自己遇到的问题,或者是提出其他不同的意见

使用在线网站将 JSON 数据转换成接口类型定义

场景:后端接口一大堆字段,一个个手动输入成接口类型费时费力。

此时 json2ts 就可以帮助你减少这部分的工作量

Mac 电脑按住 option 键直接查看接口类型定义的属性

场景:直接将鼠标放在类型名上,只能看到类型名以及注释,无法看到属性名。

此时只需要同时按住 option 键就可以看到属性名了!!!

我不会是最后一个知道这件事的吧!!!

使用索引签名支持任意数量属性

场景:有一个类型,只有几个明确的属性知道类型,还有一些不清楚类型的属性。

此时难道只能将整个类型定义成 any 嘛?非也~

可索引的类型 可以解决这个问题。

// 表示的是SquareConfig可以有任意数量的属性,并且只要它们不是color和width,那么就无所谓它们的类型是什么
interface SquareConfig {
    color?: string;
    width?: number;

    // 可索引类型,这个对象可能具有某些做为特殊用途使用的额外属性
    [propName: string]: any;
}

使用范型规范一堆类似类型

场景:后端返回的列表接口,都是一样的结构,只是具体的列表项类型不同,每次都得定义一种返回类型,太费事了。

此时你就需要 范型 来帮你解决了。

// 实际出现的坏例子
interface Result {
  total: number;
  list: any[];
}
// 这个稍微好一点,对 list 做了限定
// 但是为什么还不够好,因为每一种列表项都重复定义了一种返回结构。。。
interface Result1 {
  total: number;
  list: VO1[];
}
interface Result2 {
  total: number;
  list: VO2[];
}
// ...

// 使用范型,更规范
export interface ListResult<T> {
  list: T[];
  total: number;
}
// 使用时只需要写 ListResult<VO> 就等价于
interface Result3 {
  total: number;
  list: VO[];
}

使用 ComponentProps 提取 React 组件的 props

场景:需要扩展 Antd 的 Select 组件,此时可以使用 ComponentProps 提取 Select 组件的 props 类型:

之前的做法可能是:

// 豪放型选手:什么扩展?我只管我自己定义的属性
interface NewSelectorProps1 {
  test?: string; // 扩展之后新加的字段
}
// 小机灵鬼选手:索引类型懂吧?
interface NewSelectorProps1 {
  test?: string; // 扩展之后新加的字段
  [propName: string]: any;
}
// 科班型选手:继承懂吧?
interface NewSelectorProps2 extends SelectProps {
  test?: string; // 扩展之后新加的字段
}

另一种做法:

interface NewSelectorProps extends ComponentProps<typeof Select> {
  test?: string; // 扩展之后新加的字段
}

// ComponentProps 的定义源码
type ComponentProps<T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>> =
    T extends JSXElementConstructor<infer P>
        ? P
        : T extends keyof JSX.IntrinsicElements
            ? JSX.IntrinsicElements[T]
            : {};

这样做有什么好处呢?

  1. 如果不知道被扩展组件的 props 类型也可以完美定义扩展后的 props 类型
    1. 某些组件没有暴露出 props 类型定义
    2. 某些组件暴露出的类型和实际的类型不符
    3. 需要扩展的组件是动态的,例如 HOC 场景
  2. 省事,不用去查组件暴露出来的类型是个啥

如果有什么不建议使用的硬伤,麻烦评论区告诉我

使用 Omit 修改一个接口中的某一个属性

场景:页面表单数据类型和后端要求的传输对象类型有一个字段的类型不同,此时就可以使用 Omit 来迅速实现,而不用进行复制粘贴。

interface FormVO {
  test: number;
  // ... 其他不需要改变的属性定义
}
export interface IForm extends FormVO<FormVO, 'test'> {
  test: string; // 只将 test 修改为 string 类型
}

使用 const 断言自定义 hook 的返回值

场景:定义了一个 hook ,返回的不止一个值,结果还需要费时费力的定义每一项的类型,否则取值的时候不能正确识别到类型。

const assertions 是 3.4 版本推出的,代表一种类型断言—— 该表达式中的字面类型不应被扩展;

// 一个自定义 hook 示例
  // 类型示意
  // let map: {};
  // let updateMap: () => {};
function useTest1() {
  return [map, updateMap]; // 直接导出
}
function useTest2() {
  return [map, updateMap] as [object, () => void]; // 定义类型导出,费时费力
}
function useTest3() {
  return [map, updateMap] as const; // 使用 const 断言
}

const [map1, updateMap1] = useTest1();
const [map2, updateMap2] = useTest1();
const [map3, updateMap3] = useTest1();
// 推断出的类型分别为
// map1: object | () => void; updateMap1: object | () => void;
// map2: object; updateMap2: () => void;
// map3: object; updateMap3: () => void;

杀手级的TypeScript功能:const断言

使用重载定义同一个函数不同入参返回不同的值

场景:有一个函数,如果传入了第二个参数,则返回对象,否则返回单值

// 一种写法
interface Test {
  <T>(a: T, isObj?: boolean): T | object;
};
// 另一种写法
interface Test {
  <T>(a: T): T;
  <T>(a: T, isObj: boolean): object;
};

使用类型保护明确类型

场景:有时方法的入参类型不确定,但是需要针对不同类型来做不同的处理。

在这种场景我们就需要使用 用户自定义类型保护 来明确参数的类型。

interface Foo {
  test1: string;
}
interface Bar {
  test2: number;
}
// 一个普通的检测函数
function isFoo1(arg: Foo | Bar) {
  return (arg as Foo).test1 !== undefined;
}
// 使用 a is B 自定义类型保护,如果返回值为真,则 a 是 B 类型
function isFoo2(arg: Foo | Bar): arg is Foo{
  return (arg as Foo).test1 !== undefined;
}
function test1(arg: Foo | Bar) {
  if (isFoo1(arg)) {
    console.log(arg.test1); // Error 类型“Bar”上不存在属性“test1”
    console.log(arg.test2); // Error 类型“Foo”上不存在属性“test2”
  } else {
    console.log(arg.test1); // Error
    console.log(arg.test2); // Error
  }
}
function test2(arg: Foo | Bar) {
  if (isFoo2(arg)) {
    console.log(arg.test1); // ok
    console.log(arg.test2); // Error 属性“test2”在类型“Foo”上不存在。你是否指的是“test1”?
  } else {
    console.log(arg.test1); // Error 属性“test1”在类型“Bar”上不存在。你是否指的是“test2”?
    console.log(arg.test2); // ok
  }
}

其他的内置类型保护还有:instanceof \ typeof \ in \ 字面量类型
类型保护