Overview

到目前为止,用TS也写了不少业务了,大部分时候其实只要有 以及 类型 的概念,在用TS完成业务的过程中也就不会有太大的问题。或者有写 “C++” 或 “Java” 的经验,我们很快能从 “JS” 无痛切换到 TS。但是写久了就会发现,“TS” 最有意思的地方不是单纯的类型定义以及类型约束,或者说我们使用 “TS” 本身是为了达到类型约束的目的,类型定义只是我们的一个手段;它的 类型编程 才是在实际使用过程中最香的地方。所以我对 TS类型编程 做了一些探索。

TS 编译 JSX

TS + React 的项目里,我们通常会让TS的编译器 TSC帮助我们完成JSX的转换,在tsconfig.json的配置compilerOptions: { jsx: "" }里,我们大概会有如下选择:

ModeInputOutputOutput File Extension
preserve<div /><div />.jsx
react<div />React.createElement("div").js
react-native<div /><div />.js
react-jsx<div />_jsx("div", {}, void 0).js

这里主要列出两个常用的选项的区别:

  • preserve
    这一模式会保留JSX标签,把TS编译成JS,因此我们编译完成后并不能直接运行,还需要另外对保留下来的jsx进行处理。如果是用 Webpack 打包,则配置如下:

    rules: [
     {
      test: /\.tsx?$/,
      use: [
        {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-react"],
          },
        },
        {
          loader: "ts-loader",
        },
      ],
    },
    ],
  • react
    这一模式会直接把jsx标签编译成React处理,最后得到的是可以直接运行的js文件,在 Webpack 里只需要配置ts-loader就行了,它会调用TSC进行编译。

TS 类型编程

为了渐进式的理解TS的类型编程,我们首先需要了解它的一些类型操作符,然后在了解TS内置的一些Type utility。基于这些我们再去探索如何使用TS实现更加灵活的类型工具。

泛型

泛型是TS里面类型编程的基础,它也是提高我们类型定义的复用性的基础。

function cloneObj<T>(arg: T): T {
  return arg;
}

比如cloneObj就能被复用到各种对象的拷贝上,在类型定义里泛型可以看成是个插槽;而在类型编程里,泛型就可以看成是变量,我们可以再类型编程中使用这个变量和一些类型操作符去编写类型工具。

类型操作符

  • keyof
    这个操作符主要是会取出对象类型里的所有key然后得到一个联合类型

    type Animal = { name: string; age: number };
    
    type AnumalKey = keyof Animal; // "name" | "age"

    当我们取得对象类型里包含stringnumber的索引签名时,keyof也会把其中的stringnumber取出到union type中。

    type Thing = { [key: number]: unknown }
    
    type ThingKey = keyof Thing; // number
    
    type MultiThing = { [key: string]: unknown }
    
    type MultiThingKey = keyof MultiThing; // string | number
    // 这里我们得到 string | number 是因为在js里对象的键会被默认转换成string,也就是说obj["1"]和obj[1]取得的结果是一样的。
    
    type Obj = { name: string; [key: number]: unknown }
    
    type ObjKey = keyof Obj; // number | "name" 
  • typeof
    typeof是推断一个变量的类型的操作符,在类型编程里,我们的输入就是类型。

    const foo = () => 'foo';
    
    type Foo = typeof foo; // () => string
    
    type FooReturn = ReturnType<typeof foo>; // string
  • 索引类型
    就像在JS里我们能通过索引取得对应的值一样,在TS里我们也可以通过索引取得对应的类型,同时它也可以结合keyof使用,效果更佳。

    type Animal = { name: string; age: number; fly: boolean }
    
    type Fly = Animal["fly"]; // boolean;
    
    type AnimalVal = Animal[keyof Animal]; // string | number | boolean
  • 映射类型(Mapped Type)
    映射类型是通过in操作符遍历类型的key得到的新的类型。配合 +-操作符可以对readonly?关键字进行增加或剔除处理。

    type Animal = { name: string; age: number }
    
    type Mapped = {
    [K in keyof Animal]: boolean;
    }; // { name: boolean; age; boolean }

    我们对Mapped再抽象一下,加入泛型,如下:

    type Mapped<T> = {
    [K in keyof T]: boolean;
    }

    到这里是不是已经有类型编程那味了!
    我们再来加入+-操作符:

    type MappedToPartial<T> = {
    [K in keyof T]?: boolean
    }
    
    type MappedToRequired<T> = {
    [K in keyof T]-?: boolean;
    }
  • 条件类型(? :)
    同样和JS里的条件操作符一样,? :也可以再TS里使用,只不过它的操作对象是类型。它通常和extends一起使用,毕竟类型里是没有===, >, <这些操作符去做判断逻辑的,extends就起到了对应的作用。

    type Flatten<T> = T extends any[] ? T[number] : T;
    
    // Extracts out the element type.
    type Str = Flatten<string[]>; // string
    
    // Leaves the type alone.
    type Num = Flatten<number>; // number

    还是继续说Flatten,假如我们这样写:Flatten<string[] | number[]>,得到的结果是什么呢?到这我们不得不提到一个概念:类型分发

    • 类型分发
      对于联合类型作为条件类型的操作数时,当联合类型单个的被用为裸类型时,TS会把联合类型分发成一次一次的推断:

      type Example1 = Flatten<string[] | number[]>; // string | number

      当联合类型被用为包裹类型时,TS不会对联合类型进行分发推断:

      type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
      
      // 'StrOrNumArr' is no longer a union.
      type StrOrNumArr = ToArrayNonDist<string | number>; // (string | number)[]
      /*
       我们要区分 string[] | number[] 和 (string | number)[]的区别
       前者是指由 string数组 和 number数组 组成的联合类型
       后者是指一个数组里可以有 string 和 number 两种元素
      */

      官方文档对类型分发的解释:
      Typically, distributivity is the desired behavior. To avoid that behavior, you can surround each side of the extends keyword with square brackets.

  • infer
    infer是待推断的意思,在TS里它可以用来定义一个待推断类型变量。比如我们从一个redux的reducer里面提取state和action的类型:

    type ExtractReducer<T> = T extends (state: infer S, action: infer A) => infer S ? { state: S, action: A } : never;
    
    type RootType = ExtractReducer<typeof rootReducer>;
    
    type RootState = RootType["state"];
    type RootAction = RootType["action"];

递归类型

类型编程和普通的编程一样,也会有递归的场景,我们来看一个递归的例子:

  • DeepRequired

    type DeepRequired<T extends object> = {
      [K in keyof T]-?: T[K] extends object ? DeepRequired<T[K]> : T[K];
    }
    // 这样写看起来似乎已经完成了递归的逻辑,但是实际上是有问题的
    // 实际上 T[K] = undefined | ..., T[K] extends object always be false
    // 所以我们需要做的是把字段K变成required,再做 T[K] extends object 判断,如下:
    
    type Required<T extends object> = {
      [K in keyof T]-?: T[K];
    }
    
    type DeepRequired<T extends object> = {
      [K in keyof T]-?: Required<T>[K] extends object ? DeepRequired<T[K]> : T[K];
    }

这些基本上就是我们在类型编程中会用到的一些东西了,基于以上的这些操作符,结合递归思想,可以延伸出很多工具类型。


用户bPbopXz
67 声望0 粉丝