Typescript 技巧小结

310 阅读6分钟

基本介绍

TypeScript是一种开源的编程语言,给 JavaScript 增加了一套静态类型系统。通过 TS Compiler 编译为 JS,编译的过程做类型检查。

因为代码中添加了静态类型,也就可以配合编辑器来实现更好的提示、重构等,这是额外的好处。

image.png

TypeScript 的类型系统是图灵完备的,也就是能描述各种可计算逻辑。简单点来理解就是循环、条件等各种 JS 里面有的语法它都有,JS 能写的逻辑它都能写。

因为TypeScript支持字面量类型,并且支持逻辑运算,因此我们可以在此基础上做类型编程。

关于TypeScript的基础知识可以参考:jkchao.github.io/typescript-…

基本运算符

条件判断 extends ? :

// 判断这个类型是不是2
type  isTwo<T> = T extends 2 ? true: false;

地址

推导:infer

如果我们的不断深入,需要引入推导 infer 关键字,表示在 extends 条件语句中待推断的类型变量。

// 提取类型infer
// 推断初第一个元素的类型是C
type ArrFirst<T extends unknown[]> = T extends [infer C, ...infer R] ? C : never;

这里是将数组的类型解构,并且把数组的第一个的类型赋值为C,而这里的extends ?:就类似于if判断语句。同样的道理我们可以获取数组的最后一个

infer ,我的理解相当于创建了一个类型的名称,去存储一个类型。

地址

联合:|

联合类型(Union)类似 js 里的或运算符 |,但是作用于类型,代表类型可以是几个类型之一。

`type Union = ``1` `| ``2` `| ``3``;`

交叉:&

交叉类型(Intersection)类似 js 中的与运算符 &,但是作用于类型,代表对类型做合并

`type` `ObjType = {a: number } & {c: boolean};`

映射类型

对象 class 在typescript对应的类型是索引类型,如果我们想要修改,就需要映射类型。

type MapType<T> = {
  [Keyin keyof T]?: T[Key]
}
  1. keyof T 是查询索引类型中所有的索引,叫做索引查询
  2. T[Key] 是取索引类型某个索引的值,叫做索引访问
  3. in 是用于遍历联合类型的运算符。

比如我们把一个索引的值变为三个元素的数组

type MapType<T> = {
    [Key in keyof T]: [T[Key], T[Key], T[Key]]
}
 
type res = MapType<{a: 1, b: 2}>;

地址

尝试深入进阶

有了以上的基础之后,我们可以稍微一些进阶一点的内容

获取Promise包裹的返回值

type GetValueType<P> = P extends Promise<infer Value> ? Value : never;
type res = GetValueType<Promise<number>>

如果传入的泛型P,是一个Promise的话,我们就把Promise里面的值的类型推导为Value,并且返回Value;否则的话就返回never。

地址

获取函数的参数类型

`type` `GetParameters<Func extends Function> =`

`    ``Func extends (...args: infer Args) => unknown ? Args : never;`

地址

获取函数的返回类型

同样的思路,我们可以实现函数的返回类型

type getReturnType<Func extends Function> =
    Func extends (...args: infer Args) => infer T ? T : never;
 
type res = getReturnType<(name: string) => () => {}>

地址

删除数组中的最后一个元素

掌握了infer的思想,我们可以用解构赋值的思路去实现删除一个元素

type PopArr<Arr extends unknown[]> =
    Arr extends [] ? []
        : Arr extends [...infer Rest, unknown] ? Rest : never;

如果传入的类型是个空的数组,我们就直接返回空数组;不然的话如果是一个非空的数组,我们就将前面length-2的部分解构到Rest,后面的一个元素用unknown占位,再返回Rest,否则返回never。

地址

判断字符串的开头

除了常规的字符串的操作之外,我们还可以对字符串进行处理

我们仍然可以利用这种infer的匹配的方式


type StartsWith<Str extends string, Prefix extends string> =
    Str extends `${Prefix}${string}` ? true : false;

将字符串和前缀输入,判断Str是否是匹配Prefix{string}这样的字符串,前面的Prefix代表前缀,后面的string代表任意字符串

地址

字符串的替换

我们已经可以实现将字符串进行分别的匹配,换言之我们可以拿到不同的部分,因次我们将源字符串进行拆解之后重新拼装

type ReplaceStr<
    Str extends string,
    From extends string,
    To extends string
> = Str extends `${infer Prefix}${From}${infer Suffix}`
        ? `${Prefix}${To}${Suffix}` : Str;

现将字符串进行extends判断,将字符串的前缀,替换字符、后缀分别赋值,在重新构造字符串

地址

字符串替换–递归

在常规的js运算中我们可以使用递归来做多次处理,ts中也是一样的

type ReplaceStr<
    Str extends string,
    From extends string,
    To extends string
    > = Str extends `${infer Prefix}${From}${infer Suffix}`
    ? ReplaceStr<`${Prefix}${To}${Suffix}`, From, To> : Str;
 
 
type res1 = ReplaceStr<'abcdabcdabcd', 'd', 'f'>

我们将从新拼接的字符串重新作为入参进行递归判断,直到不满足extends则直接返回Str。

地址

字符串的进行首字母大写

我们要调用首字母大写,就需要先匹配到第一个字母,将字母转换之后再拼接返回

type CapitalizeStr<Str extends string> =
    Str extends `${infer First}${infer Rest}`
        ? `${Uppercase<First>}${Rest}` : Str;

我们取出字符串的第一个字符,再用Uppercase去将第一个变化为大写,最后返回拼接的字符串。

这里我们用的字符串的匹配,如果没有任何的分隔符,我们将会匹配到字符串的第一个字母。

地址

\

CamelCase

我们来实现 dong_dong_dong 到 dongDongDong 的变换

type CamelCase<Str extends string> =
    Str extends `${infer Left}_${infer Right}${infer Rest}`
        ? `${Left}${Uppercase<Right>}${CamelCase<Rest>}`
        : Str;

我们来讲讲这其中关于字符串的匹配

拿到一个字符串,先用分隔符"_"去匹配,Left则是“dong”,然后再对右侧去匹配Right指的是第一个字母“d”,Rest则是后面。因为涉及到很多元素,因此需要递归

地址

多说一句

比如我们查看Uppercase的实现,发现是intrinsic,intrinsic代表了这些工具类型是由TS编译器内部实现的。

计数思路

在TypeScript中,我们可以利用数组的length属性做计数

type num1 = [unknown]["length"];
type num2 = [unknown, unknown]["length"];

构建指定长度的数组

type BuildArray<
  Length extends number,
  Ele = unknown,
  Arr extends unknown[] = []
> = Arr["length"] extends Length ? Arr : BuildArray<Length, Ele, [...Arr, Ele]>;

我们为Arr默认值[],通过判断length来判断是否返回数组,不然的话就递归调用,再为数组添加元素[...Arr, Ele]

地址

数字加法

我们已经实现了构建指定的数组,那么我们就需要购将这两个数组,再合并起来,读取length

type Add<Num1 extends number, Num2 extends number> = [
  ...BuildArray<Num1>,
  ...BuildArray<Num2>
]["length"];

数字减法

type Subtract<
  Num1 extends number,
  Num2 extends number
  > = BuildArray<Num1> extends [...BuildArray<Num2>, ...infer Rest]
  ? Rest["length"]
  : never;
 
type SubtractRes1 = Subtract<100, 20>;

数字减法的思路是我们构建数字较大的数组(被减数),再去匹配一个一模一样的数组(减数),减数的数量不够的话肯定需要一定长度的数组去匹配,这样读出Rest的长度即可

数字乘法

乘法就是加法的简便运算,原来一个一个加,现在变为了n个n个的加。比如m*n,其实就是n个m相加,这里的话就是一次一次的添加m个元素,重复n次。

// 递归做乘法
type Multiplication<
  Num1 extends number,
  Num2 extends number,
  Result extends unknown[] = []
> = Num2 extends 0
  ? Result["length"]
  : Multiplication<Num1, Subtract<Num2, 1>, [...BuildArray<Num1>, ...Result]>;
 
type MultiplicationRes = Multiplication<100, 20>;

数字除法

除法和乘法想法,其实就是n个n个的减,每减一次,结果加1,直到减为0。


// 递归做除法
type Division<
  Num1 extends number,
  Num2 extends number,
  Result extends unknown[] = []
> = Num1 extends 0
  ? Result["length"]
  : Division<Subtract<Num1, Num2>, Num2, [unknown, ...Result]>;
 
type DivisionRes = Division<20, 4>;

结语

经过以上的举例,我们已经有了有了一定的类型编程能力,可以进行更多锻炼。我们可以看看Pick Partial Omit等内部的实现,发现其实并不复杂。

Github上面有个仓库,里面有很多学习的案例,可以更深层次学习下。

author:@念歌