Ts 类型编程(一)

441 阅读5分钟

从本篇开始,我们所有要学习的如何熟悉 Ts 并可以像驾驭 Js 一样轻车熟路。

infer

其实看到提取便应该想到在上一章中提到过的 infer ,我们再看下那个例子:

type TOT<Tuple extends unknown[]> = Tuple extends [infer T, ...infer R] ? T : never;
// 推导 - 提取 T
type toT = TOT<[1,2,3,4,5]> // type toT = [1]

// 推导 - 提取 R
type TOR<Tuple extends unknown[]> = Tuple extends [infer T, ...infer R] ? R : never;

type toR = TOR<[1,2,3,4,5]> // type toR = [2,3,4,5]

我们看到通过 infer 拿到了想要获取的所有参数。

或许你又会想到,,它好像和我们在开发中使用的正则有些类似,通过定义的规则寻找 到可匹配的部分并进行值的替换。

我们可以回忆一下正则在开发中的一些使用场景,如果你已经很熟悉正则的使用则可以跳过这一节:

// 早在 JQ 的时代,我们通常会用正则来提取模板中的变量并去替换这些变量为我们所要填充的数据

// 挂载的节点
  <div class="oWrap"></div>

// 模板 是不是很熟悉 像是在填充框架
  <script type="text/html" id="tpl">
    <div class="box">
      <h3>{{title}}</h3>
      <span>{{content}}</span>
    </div>
  </script>

<script type="text/javascript">
    const oWrap = document.getElementsByClassName('oWrap')[0],
      oTpl = document.getElementById("tpl").innerHTML,
      RegExp = /{{(.*?)}}/g;
    // console.log(oTpl);
    oWrap.innerHTML = oTpl.replace(RegExp, function (node, key) {
    // console.log(node, key)  // 如果不熟悉replace的使用,可以打开打印或者查看MDN
      return {
        title: "RegExp",
        content: "exercrse"
      }[key]
   })
</script>

此时我们便可以看到页面了,同时看到了我们想要的结果:

image.png

我们来看一下当打开注释时,replace 的回调分别输出了什么:

image.png

其实就是模板变量和我们要填充的数据的key值。

这是我们通过正则做到了对参数的提取编辑,而在 Ts 中我们想要做到对数据的提取便需要用到 infer 了。接下来我们一起来熟悉 infer 的使用规则: 首先需要知道 infer 的几个点:

  1. 只能作为条件类型的 extends 子句中使用
  2. 得到的类型只能是为 true 的状态语句中使用

提取字符串字面量类型:

type inferStr<T extends string> = 
    T extends  `${infer firstLetter}${infer residue}` 
        ? firstLetter : [];

// 提取第一个字母 - A
type letter = inferStr<'ABC'>

我们将字符串的第一个字母通过infer提取出来,并通过变量firstLetter来接收这个值,最后结合条件类型将值返回。

既然可以取到第一个字母,也就意味着可以取到任意位置的,我们可以通过多个插值配合infer来进行提取,而后返回变量。

提取数组、元组类型:

type inferAry<T> = T extends (infer _)[] ? _ : never;
// 需要注意这里  (infer _)[] ,刚开始可能不熟悉语法 后边看多了回头看就清楚了

// 提取元组的类型 - number
type tupleType = inferAry<[number, number]>;
// 提取数组的类型 - number
type tupleType = inferAry<number[]>;

那提取它们的某个元素呢?很简单对吧

type inferRes<T extends unknown[]> = 
    T extends [infer first, ... infer res, infer last] 
        ? res : never;

// 这样我们便拿到了 [2,3,4]
type result = inferRes<[1,2,3,4,5]>

提取函数信息:

经过以上的理解 不知是否可以尝试自己来提取函数的参数类型和返回值呢。

// 提取函数参数类型
type inferParams<T extends Function> =
    T extends (...args: infer res) => any 
        ? res : never;

// 这样我们便可以拿到参数的具体类型 [x: number, y: number]
type ArgsParams = inferParams<((x: number, y: number) => void)>;
// 参数可以不包括号,直接作为参数传入即可,也都可以拿到理想的返回值

------------------------------
// 提取返回值类型
// 如果没有想到参数的提取 那经过以上的练习是否想到了返回值的提取了呢?

type inferReturn<T extends Function>  =
    T extends (x: number, y: number) => infer res 
        ? res : never;

// number
type Return = inferReturn<() => number>

提取Promise成功时的返回值类型

type inferPromise<T> = T extends Promise<infer R> ? R : never;

// boolean
type Resolve = inferPromise<Promise<boolean>>

到现在我们可以说是熟悉了 infer 在不同状态的使用方式了。接下来,我们便可以尝试实现一些数组、字符串方法了:

  • shift
type shift<T extends unknown[]> =
        T extends [] ? [] 
             : T extends [infer first, ...infer res] 
             ? [res] : [];
  • pop 与 shift 等同,这里就不说了。
  • reverse 也是一样的。
type reverse<T extends unknown[], Ary extends unknown[] = []> =
    [] extends T
    ? Ary
        : T extends [...infer L, infer R]
        ? reverse<L,[...Ary, R]> // 这里可以自己举个例子看一下
    : Ary;  

type arr = reverse<[1,2,3,4,5,6]>

这里我们通过 extends 对数组 T 进行了约束,由于数组类型是未知的,所以在这里将 T 约束为未知类型的数组。同理,我们以同样的方式定义了返回值:Ary extends unknown[]。先判断 T 是否是 [] 的子类型:否则直接返回空数组;是则对 T 进行提取,我们将数组的最后一个元素提取出来并通过递归的方式对剩余数组元素继续进行提取重组,直到所有元素替换完成。

  • trim
type trim<str extends string> =
str extends `${" " | "\n" | "\t"}${infer res}${" " | "\n" | "\t"}`
    ? trim<res> : str

type s = trim<'      sdf      '>

这样一看,我们好戏将两端的空白进行修剪了,持续递归从而做到想要的结果。

可是我们现在这种写法存在弊端:如果两侧空格数目并不相等时,当一侧被处理干净则必定会导致另一端依旧未修剪完毕。

所以,我们需要将两侧清除的过程抽离出来,避免他们互相影响:

type trimL<str extends string> =
str extends `${" " | "\n" | "\t"}${infer res}`
    ? trimL<res> : str

type sl = trimL<'      sdf       '>


type trimR<str extends string> =
str extends `${infer res}${" " | "\n" | "\t"}`
    ? trimR<res> : str

type sr = trimR<'      sdf       '>

这样我们分别实现了两端 tirm,然后我们再嵌套调用:

type trimStr<str extends string> = trimL<trimR<str>>
// 或者
type trimStr<str extends string> = trimR<trimL<str>>

这样我们便可以实现对于字符的两侧的空白进行修剪了,并且也不会因为一侧的修剪完毕导致的另一端可能存在的未修剪干净的现象。

最后我们再补充一些:

补充:

需要知道的是,在函数中关于参数类型可以是任意类型,即:any。(但是不可以用 unknown,会涉及到参数的逆变性质,然后会有单独的 一篇来讲)

关于构造器:

我们上边讲到了函数参数、返回值类型的提取,构造器 与之其实没什么区别。然后这里我们补充一点注意事项:

构造器类型可以通过 interface 声明,Ts 也提供了语法:

interface Person {
    name: string;
    age: number;
}

interface PersonCons {
    new (name: string, age: number): Person 
    // 这里指的是:限制传参类型,返回值是Person类型的实例对象
}

我们再来通过 infer 提取一下构造函数返回值类型对象 - Person,其实就是拿到返回值和我们在函数那里拿返回值类型是一样的:

type getInstancType<
	    Func extends new (...args: any) => any 
    > =
	Func extends new (...args: any) => infer instanceType
	? instanceType : any

// 这样我们就拿到了函数的返回值类型对象
type ins = getInstancType<PersonCons>

总结:

  1. 我们可以通过infer做到类似于正则的一些能力;
  2. 在函数中关于参数类型可以是任意类型,即:any。 (但是不可以用 unknown,会涉及到参数的逆变性质)