类型体操小册学习总结ing

147 阅读17分钟

参考:神光

TS类型系统中的类型

静态类型系统的目的是把类型检查ongoing运行时提前到
编译时,那TS类型系统中肯定要把JS的运行时类型拿过来
也就是number,boolean,string,object,bigint,symbol
undefined.null这些类型,还有就是他们的包装类型Numner
Boolean,String,Object.Symbol

复合类型方面,JSClass,Array这些ts也支持.但是又多了
三种类型.元组(Tuple)接口(Interface)枚举(Enum)

元祖 就是元素个数和类型固定的数组类型

type Tuple = [number,string]

接口 可以描述函数.对象.构造器的结构

对象

type Ipersons = {
    name:string
    age:number
}
interface Ipersons2{
    name:string
    age:number
}
class Person implements Ipersons {
    name:string
    age:number
}

const obj9:Ipersons = {
    name:'asdad',
    age:656
}

函数

type SayHello = {
    (name:string):string
}

const func: SayHello = (name:string) =>{
    return 'hello' + name
}

构造器

type PersonConsructor = {
    new (name:string,age:number):Ipersons
}

function createPerson(ctor:PersonConsructor):Ipersons{
    return new ctor('gang',10)
}

对象类型,class类型在TypeScript里也叫做索引类型,也就是索引了多个

  • 元素的类型的意思,对象可以动态添加属性,如果不知道会有什么属性
  • 也可以用索引签名
interface Iperson3 {
    [prop:string]:string|number
}

const obj2:Iperson3 = {}
obj.name = 'asdasd'
obj.age=12312
  • 总之接口可以用来描述函数,构造器.索引类型等符合类型

枚举 是一系列值的整合

enum Transpiler {
    Webpack ='webpack',
    rollup = 'rollup',
    vite='vite',
    npm ='npm',
    yarn='yarn',
    pnpm='pnpm'
}

const transpiler = Transpiler.yarn
  • 除此之外,TypeScript还支持字面量类型.也就是类似111,'aaaa',{a:"1"}
  • 其中.字符串的字面量类型有两种.一钟是普通的字符串字面量
  • 一种是普通的字符串字面量 比如 'aaa'
  • 另一种是模板字面量 比如 aaa${string} 他的意思是以aaa开头
  • 后面是任意string的字符串字面量类型
  • 所以想要约束以某个字符串开头的字符串字面量类型时可以这样写
function fn1(str:`#${string}`){

}
fn1('#aaa')
fn1('#asd')

还有四种特殊的类型 void never any unknown

  • void 代表为空.可以是null或者undefined 一般是用于函数返回值
  • any是任意类型,任何类型都可以赋值给他,它也可以赋值给任何类型(除了never)
  • unknown是未知类型.任何类型都可以赋值给它,他可以赋值给任何类型(除了never)
  • never代表不可达 比如函数抛异常的时候 返回值就是never
  • 以上就是TS类型系统的所有类型了

类型装饰

  • 除了描述类型的结构外,TS的类型系统还支持描述类型的属性
  • 比如是都可选.是否可读等
interface Iperson4 {
    readonly name:string
    age?:number
}

type tuple = [string,number]
  • TS类型系统中的类型运算
  • 条件 extends?:
  • TS中的条件判断是 extends?: 叫做条件类型(ConditionalType)
type res = 1 extends 2?true:false  // 类型为false

// 这就是TS类型系统的if else
// 但是 上面这样的逻辑没啥意义,静态的值自己就能算出结果来
// 为什么要用代码去判断呢

// 所以,类型运算逻辑都是用来做一些动态的类型的运算.也就是对类型参数的运算

type isTwo<T> = T extends 2 ? true:false

type res2 = isTwo<1>
type res3 = isTwo<2>
  • 这种类型也叫做高级类型
  • 高级类型的特点是传入类型参数,经过一系列类型运算逻辑后.返回新的类型

推导: infer

  • 如何提取类型的一部分呢,答案是infer
  • 比如提取元祖类型的第一个元素
type First<Tuple extends unknown[]> = Tuple extends [
infer T,...infer R] ? T : never
type res5 = First<[1,2,3]>
  • 注意,第一个extends不是条件,条件类型是extends?: 这里的extends
  • 是约束的意思,也就是约束类型参数只能是数组类型
  • 因为不知道数组元素的具体类型,所以用unkown

联合

  • 联合类型(Union)类似js里的或运算符| 但是作用于类型
  • 代表类型可以是几个类型之一
type Union = 1 | 2 | 3

交叉 &

  • 交叉类型(intersection)类似js中的与运算符&,但是作用于类型
  • 代表对类型做合并
type ObjType = {a:number} & {c:boolean}

type res6 = {a:number,c:boolean} extends ObjType ?true:false
  • 注意,同一类型可以合并,不同的类型没法合并,会被舍弃
type rs = 'aaa' & 222

映射类型

  • 对象 class在TypeScript对应的类型是索引类型(indexType)
  • 那么如何对索引类型做修改呢 就要用到用到映射类型
// type MapType<T> ={
//     [key in keyof T]?:T[Key]
// }

// keyof T是查询索引类型中所有的索引 叫做索引查询
// T[Key]是取索引类型某个索引的值,叫做索引访问
// in 使用与遍历联合类型的运算符
// 比如我们把一个索引类型的值变成3个元素的数组

type MapType<T> ={
        [Key in keyof T]:[T[Key],T[Key],T[Key]]
    }

type rs2 = MapType<{a:1,b:2}>

// 映射类型就相当于把一个集合映射到另一个集合,这是他名字的由来

// 除了值可以变化 索引也可以做变化 用as运算符 叫做重映射

type MyType<T> = {
    [
        Key in keyof T
         as `${Key & string}${Key & string}${Key & string}`
    ] :[T[Key],T[Key],T[Key]]
}

type rs3 = MyType<{a:1,b:2}>
  • 我们知道普通对象的键都是字符串类型,所以这里使用了Key & string交叉运算
  • 因为Js处理对象比较多.所以索引类型的映射比较重要

小结

  • 给JavaScript添加静态类型系统,那肯定是能复用的就复用,所以在TS中

  • 基础类型和class Array等复合类型都是和JS一样的,只是又额外加了接口

  • (interface),枚举(enum) 元祖(turple)这三种复合类型

  • 对象类型 class类型在ts中也叫做索引类型,还有void never any unknow

  • 四种特殊类型,以及支持字面量作为类型,此外,ts类型系统也支持通过

  • readonly,?等修饰符对属性的特性做进一步的描述

  • 此外 ts支持对类型做运算,这是它的类型系统的强大之处,也是复杂之处

  • ts支持条件,推导,联合.交叉等运算逻辑,还有对联合类型做映射

  • 这些逻辑是针对类型参数.也就是泛型来说的,传入类型参数.经过一系列

  • 类型运算逻辑后,返回新的类型的类型就叫做高级类型

  • 如果是静态的值,直接算出结果即可,没必要写类型逻辑

  • 这些语法看起来没有多复杂,但是他们却可以实现很多复杂逻辑

  • 就像JS的语法也不复杂,却可以实现很多复杂逻辑一样 <===============================================================>

  • Ts类型编程的代码看起来比较复杂.但其实这些逻辑用JS都能写

  • 接下来是类型体操的第一个套路

模式匹配做提取

  • 我们知道 字符串可以和正则做匹配,找到匹配的部分,提取子组
  • 之后可以用1,2等引用匹配的子组
'abc'.replace(/a(b)c/,`$1,$!,$1`)
  • TS的类型也同样可以做模式匹配
  • 比如这样一个Promise类型
type p = Promise<'gang'>
// 我们像提取value的类型,可以这样做
    type GetValueType<P> = P extends 
    Promise<infer Value>?Value:never

    type GetValueResult = GetValueType<Promise<'gang'>>
  • 这就是ts类型的匹配

  • ts类型的模式匹配是通过extends对类型参数做匹配,结果

  • 保存到通过infer声明的局部类型变量里,如果匹配就能从该

  • 局部变量里拿到提取出的类型

  • 我们看下这个套路的具体用法

数组类型

    // First. 数组类型想提取第一个元素的类型怎么做
    type arr = [1,2,3]
    // 用它来匹配一个模式类型.提取第一个元素的类型到通过infer声明的局部变量里返回
    type GetFirst<Arr extends unknown[]> =
      Arr extends [infer First, ...unknown[]]?First :never
    type Firsts = GetFirst<[1,2,3]>  // 返回类型1
  • 类型参数Arr通过extends约束为只能是数组类型,数组元素unknow也就是可以是任何值

  • any和unknow的区别: any和unknow都代表任意类型,但是

  • unknow只能接受任意类型的值,而any除了可以接受任意类型的值

  • 也可以赋值给任意类型(除了Never),类型体操中经常用unknown接受

  • 和匹配任何类型,而很少把任何类型赋值给某个类型变量

  • 对Arr做模式匹配,把我们要提取的第一个元素的类型,放到通过infer声明的First局部变量里,后面的元素可以使任何类型,用unknown接收,然后把局部变量First返回 type Firsts = GetFirst<[]> // 返回类型never

  • Last 可以提取第一个元素,当然也可以提取最后一个元素,修改模式类型就行

    type GetLast<Arr extends unknown[]> = Arr extends 
    [...unknown[],infer Last]?Last:never
    type Lasts = GetLast<[1,2,3]>  // 类型为3 

PopArr

上面分别取了首尾元素,当然也可以取剩余的数组,比如去掉了最后一个元素的数组

   type PopArr<Arr extends unknown[]> = Arr extends [] 
     ? [] : Arr extends[...infer Rest,unknown] ? Rest:never
   // 如果是空数组,直接返回,否则匹配剩余的元素,放到infer声明的局部变量
   // Rest里,返回Rest
   // 当类型参数Arr为[1,2,3]时

   type Rests = PopArr<[{},1,2,3]>  // 类型为 [{},1,2]

   // 当Arr为空数组或者只有一个参数时,类型为[]

ShiftArr

  • 同理可得ShiftArr的类型
type ShiftArr<Arr extends unknown[]> = Arr extends []?
    [] : Arr extends [unknown,...infer Less]?Less:[]

    type Less = ShiftArr<[1,2,3,{}]>  //  类型为 [2,3,{}]

    // 当Arr参数为[]或者只有一个参数时类型都为[]

字符串类型

  • 字符串类型也同样可以做模式匹配,匹配一个模式字符串 把需要提取的
  • 部分放到infer声明的局部变量里

StartWith

  • 判断字符串是否是以某个前缀开头,也是通过模式匹配
    type StartWith<Str extends string, Prefix extends string> = Str extends
    `${Prefix}${string}`?true:false

    // 需要声明字符串Str, 匹配的前缀Prefix两个类型参数,他们都是string
    // 用Str去匹配一个模式类型,模式类型的前缀是Prefix,后面是任意的string
    // 如果匹配返回true,否则返回false

    // 当匹配时
    type StartWithResult = StartWith<'jerson and wei','jerson'>  // 匹配为true

Replace

  • 字符串可以匹配一个模式类型,提取想要的部分,自然也可以用这些再构建一个新的类型
    // 比如实现字符串替换
    type ReplaceStr<Str extends string,
                    From extends string,
                    To extends string> = 
                    Str extends `${infer Prefix}${From}${infer Suffix}`
                    ? `${Prefix}${To}${Suffix}`: Str

    // 声明要替换的字符串为Str,待替换的字符串From.替换成的字符串3个类型参数                
    // 通过extends约束都是string类型

    // 用Str去匹配模式串,模式串由From和之前之后的字符串构成,把之前之后的字符串
    // 放到通过infer声明的局部变量Prefix,Suffix里

    // 用Prefix,Suffix加上替换到的字符串To构造成新的字符串类型返回
    // 当匹配时

    type ReplaceResult = ReplaceStr<"gang is gang is ?",'?','gang'>  
    // 匹配时就会将匹配到的字符串进行替换   否则仍然是替换前的字符串

Trim

  • 能够匹配和替换字符串 那也就能实现去掉空白字符的Trim
  • 但是我们不知道有多少个空白字符,所以需要使用递归

先实现 TrimRight

type TrimStrRight<Str extends string> = Str extends 
    `${infer Rest}${' '| '\n' | '\t'} `
    ? TrimStrRight<Rest>:Str

    // 类型参数Str是要Trim的字符串
    // 如果Str匹配字符串+空白字符(空格,换行,制表符),那就把字符串放到infer声明的
    // 局部变量Rest里
    // 把Rest作为类型参数递归TrimRight,直到不匹配.这时的类型参数Str就是处理结果

    type TrimRightResult = TrimStrRight<'gang          '>  // 递归清除右边的空格

TrimLeft 同理可以得到清除左边的空格

type TrimStrLeft<Str extends string> = Str extends
    `${' ' | '\n' | '\t'}${infer Rest}`?TrimStrLeft<Rest>:Str

    type TrimLeftResult = TrimStrLeft<'     gang'>    // 模式匹配清除左边的空格

TrimRight和TrimLeft结合就是Trim

type Trim<Str extends string> = TrimStrRight<TrimStrLeft<Str>>

    type TrimResult = Trim<'   gang    '>

函数

  • 函数同样也可以做类型匹配,比如提取参数,返回值的类型

GetParameters

  • 函数类型可以通过模式匹配来提取参数的类型
type GetParameters<Func extends Function> = Func extends
(...args: infer Args) => unknown ? Args:never
// 类型参数Func是要匹配的函数类型,通过extends约束为Function
// Func和模式类型做匹配,参数类型放到用infer声明的局部变量Args里
// 返回值可以是任何类型,用unknown

// 返回提取到的参数类型Args
type ParametersResult = GetParameters<(name:string,age:number) =>string >

GetReturnType

  • 能提取参数类型,同样可以提取返回值类型
type GetReturnType<Func extends Function> = Func extends
(...args:any[]) => infer ReturnType?
ReturnType:never
// Func和模式类型做匹配,提取返回值到通过infer声明的局部变量ReturnType里返回

// 参数类型可以是任意类型,也就是any[](注意,这里不能用unknown,因为参数类型是要赋值给)
// 别的类型的,而unknown只能用来接收类型,所以用any

type ReturnTypeResult = GetReturnType<()=>'dont'>

GetThisParameterType 方法里可以调用this

  • 比如这样
class Dong {
    name:string

    constructor(){
      this.name = 'ding'
    }

    wei(){
      return '喂,我叫: ' + this.name
    }
}

const dong = new Dong()
// dong.wei()

// 用对象.方法名的方式调用的时候.this就指向那个对象
// 但是方法也可以用call 或者apply调用
// dong.wei.call({xxx:1})

// call调用的时候,this就变了.但这里却没有被检查出来this指向的错误

// 如何让编译器能够检查出this指向的错误呢
// 可以在方法声明时指定this的类型
class Ding {
  name:string;

  constructor(){
    this.name = 'ding'
  }

  wei(this:Ding){
    return '喂,我叫 ' + this.name
  }
}
// 这样,当call/apply调用的时候,就能检查出this指向的对象是否是对的

const ding = new Ding()

ding.wei()

ding.wei.call({xxx:1})

// 这里的this类型同样也可以通过模式匹配提取出来

type GetThisParameterType<T> = 
    T extends (this: infer ThisType,...args:any[]) => any
    ? ThisType : unknown

    // 类型参数T是待处理的类型
    // 用T匹配一个模式类型,提取this的类型到infer声明的局部变量
    // ThisType中,其余的参数是任意类型,也就是any,返回值也是任意类型

    // 返回提取到的ThisType 这样就能提取出this的类型

    type GetThisParameterTypeRes = 
    GetThisParameterType<typeof ding>

构造器

  • 构造器和函数的区别是,构造器是用于创建对象的.所以可以被new
  • 同样,我们也可以通过模式匹配提取构造器的参数和返回值的类型
  • GetInstanceType 构造器类型可以用interface声明,使用new():xx的语法
interface Person {
    name:string
}

interface PersonConstructor {
    new(name:string): Person
}

// 这里的PersonConstructor返回的是Person类型的实例对象
// 这个也可以通过模式匹配出来

type GetInstanceType <
    ConstructorType extends new(...args:any) => any
    > = ConstructorType extends new(...args:any) => 
    infer InstanceType ? InstanceType : any 

    // 类型参数ConsructorType是待处理的类型.通过extends约束为构造器类型

    // 用ConstructorType匹配一个模式类型,提取返回的实例类型到infer声明的
    // 局部变量InstaceType里.返回InstanceType
    // 这样就能取出构造器对应的实例类型
    type GetInstanceTypeRes = GetInstanceType<PersonConstructor>

索引类型

  • 索引类型也同样可以用模式匹配提取某个索引的值的类型,这个用的也挺多的
  • 比如React的index.d.ts里的PropsWithRef的高级类型,就是通过模式匹配提取了ref的值的类型
type PropsWithRef<P> = 
    'ref' extends keyof P
        ? P extends {ref?: infer R | undefined}
            ? string extends R 
                ? PropsWithRef<P> & {ref: Exclude<R,string> | undefined }
                : P
            : p 
        : P        
  • 我们简化一下那个高级类型.提取Props里ref的类型

GetRefProps

  • 我们同样通过模式匹配的方式提取ref的值的类型
type GetRefProps<Props> = 
        'ref' extends keyof Props 
            ? Props extends {ref?: infer Value | undefined}
                ? Value
                : never
            : never

    // 类型参数Props为待处理的类型

    // 通过keyofProps取出Props的所有索引构成的联合类型 判断下ref
    // 是否在其中,也就是`ref` extends keyof Props 

    // 为什么要做这个判断.上面注释里写了
    // 在ts3.0里面如果没有对应的索引,Obj[key]返回的是{}而不是never,所以这样坐下兼容处理

    // 如果有ref这个索引的话,就通过infer提取Value的类型返回,否则返回never
    type GetRefPropsRes = GetRefProps<{ref?: 1,name:'ding'}>

    // 当ref为undefined时
    type GetRefPropsRes2 = GetRefProps<{ref?: undefined,name:'ding'}>

小结

  • 就像字符串可以匹配一个模式串提取子组一样,

  • TS类型也可以匹配一个模式类型提取某个部分的类型

  • TS类型的模式匹配时通过类型extends一个模式类型,把需要提取的部分放到

  • 通过infer声明的局部变量里,后面可以从这个局部变量拿到类型做各种后续处理

  • 模式匹配的套路在数组,字符串,函数,构造器,索引类型,Promise等类型中都有大量的应用

  • 掌握好这个套路能提升很大一截类型体操水平

  • 类型编程主要的目的就是对类型做各种转换,那么如歌对类型做修改呢

  • TS类型系统支持3种可以声明任意类型的变量:type,infer类型参数

    // type 叫做类型别名,其实就是声明一个变量存储某个类型
    type ttt = Promise<number>
    // infer用于类型的提取,然后存到一个变量里,相当于局部变量
    type GetValueType3<P> = P extends Promise<infer Value> ? Value:never
    // 类型参数用于接收具体的类型.在类型运算中也相当于局部变量
    type isTwo<T> = T extends 2 ?true:false
    // 但是,严格来说这三种也都不叫变量,因为它们不能被重新赋值
    // ts设计可以做类型编程的类型系统的目的就是为了产生各种复杂的类型,拿不拿修改怎么产生新类型

重新构造

  • ts的type.infer类型参数声明的变量都不能修改,想对类型做各种变换产生
  • 新的类型就需要重新构造
  • 数组,字符串,函数等类型的重新构造比较简单
  • 索引类型,也就是多个元素的聚合类型的重新构造复杂一些,涉及到了映射类型的语法

数组类型的重新构造

push

  • 有这样一个元祖类型
type tuple = [1,2,3]
    // 我想给这个元祖类型再添加一些类型,怎么做呢

    // TS类型变量不支持修改,我们可以构造一个新的元祖类型
    type Push<Arr extends unknown[], Ele> = [...Arr,Ele]

    // 类型参数Arr是要修改的数组/元组类型,元素的类型任意.也就是unknown 
    // 类型参数Ele是添加的元素的类型
    // 返回的使用Arr已有的元素加上Ele构造的新的元组类型

    type PushResult = Push<[1,2,3],4>

    // 这就是数组/愿足矣的重新构造
    
    // 数组和元组的区别, 数组类型是指任意多个同一类型的元素构成的.比如number[]
    // Array<number> 而元组则是数量固定,类型可以不同的元素构成的.比如[1,true.'guang']

Unshift

  • 可以在后面添加 同样也可以在前面添加
type Unshift<Arr extends unknown[],Ele> = [Ele,...Arr]

    type UnshiftResult = Unshift<[1,2,3],4>

    // 这两个案例比较简单,下面是个复杂的

    // 有这样两个元组
    type tuplel = [1,2]

    type tuplel2 = ['shen','zhen']
    // 我们想把它们合并成这样的元组
    type tuples = [[1,'shen'],[2,'zhen']]
    
    type Zip<One extends [unknown,unknown],Other extends [unknown,unknown]>
    = One extends [infer OneFirst,infer OneSecond]
    ? Other extends [infer OtherFirst,infer OtherSecond]
        ? [[OneFirst,OtherFirst],[OneSecond,OtherSecond]]
    : []
        : []
    
    // 两个类型参数One,Other是两个元组,
    // 类型是[unknown,unknown],代表2个任意类型的元素构成的元组

    // 通过infer分别提取One和Other的元素到infer声明的局部变量
    // OneFirst,OneSecond,OtherFirst,OtherSecond

    // 用提取的元素构造成新的元组返回即可
    type tuples3 = Zip<tuplel,tuplel2>   //    [[1,'shen'],[2,'zhen']]

    // 但是这样只能合并两个元素的元组.如果是n个呢,那就只能用递归了
    type Zip2<One extends unknown[],Other extends unknown[]> = 
    One extends [infer OneFirst,...infer OneRest]
        ? Other extends [infer OtherFirst,...infer OtherRest] 
            ? [[OneFirst,OtherFirst],...Zip2<OneRest,OtherRest>]
        : [] 
            : []

    // 类型参数One.Other声明为unknown[],也就是元素个数任意.类型任意的数组

    // 每次提取One和Other的第一个元素OneFirst,OtherFirst,剩余的放到
    // OneRest和OtherRest中递归处理

    // 这样,就能处理任意个数元组的合并
    type tuplel4 = Zip2<[1,2,3,4,6],['a','b','c','d','e']>

字符串类型的重新构造

  • CapitalizeStr 我们想把一个字符串字面量类型的'dong'转换成首字母大写的'Dong' ,需要用到字符串类型的提取和重新构造
type CapitalizeStr<Str extends string> =
            Str extends `${infer First}${infer Rest}`
                ? `${Uppercase<First>}${Rest}`
                    : Str 
    
    // 我们声明了类型参数Str是要处理的字符串类型,通过extends约束为string
    // 通过infer提取出首个字符到局部变量First.提取后面的字符到局部变量Rest

    // 然后使用TS提供的内置高级类型Uppercase把首字母转为大写
    // 加上Rest,构造成新的字符串类型返回

    type CapitalizeResult = CapitalizeStr<'dong'>

    // 这就是字符串类型的重新构造:从已有的字符串类型中提取出一些部分字符串
    // 经过一系列变换,构造成新的字符串类型

CamelCase

我们再来实现 dong_dong_dong到dongDongDong的变换
// 同样是提取和重新构造
type CamelCase<Str extends string> = 
    Str extends `${infer Left}_${infer Right}${infer Rest}`
    ? `${Left}${Uppercase<Right>}${CamelCase<Rest>}`
        :Str

// 类型参数Str是待处理的字符串类型,约束为string
// 提取_之前和之后的两个字符到infer声明的局部变量Left和Right
// 剩下的字符放到Rest里

// 然后把右边的字符Right大写和Left构造成新的字符串,剩余的字符
// Rest要继续递归的处理,这样就完成了从下划线到驼峰形式的转换

type CamelCaseResult = CamelCase<'dong_dong_dong'>

// DropSubStr 可以修改自然也可以删除,示例如下
type DropSubStr<Str extends string, SubStr extends string> = 
Str extends `${infer Prefix}${SubStr}${infer Suffix}`
    ? DropSubStr<`${Prefix}${Suffix}`,SubStr>
        : Str

*** ArrInfig

  • 数Str是待处理的字符串,SubStr是要删除的字符串,都通过extends约束为string

  • 过模式匹配提取SubStr之前和之后的字符串到infer声明的局部变量Prefix,Suffix中

  • 如果不匹配就直接返回Str

如果匹配,那就用Prefix,Suffix构造成新的字符串,然后继续递归删除SubStr,直到不再匹配

    type DropResult = DropSubStr<'dingdong','dong'>

数类型的重新构造

    // AppendArgument  我们可以在函数类型上添加一个参数

    type AppendArgument<Func extends Function,Arg > =
        Func extends (...args:infer Args) => infer ReturnType 
            ? (...args:[...Args,Arg]) => ReturnType
                : never 

    // 类型参数Func是待处理的函数类型,通过extends约束为Function,Arg是要添加的参数类型

    // 通过模式匹配提取参数到infer声明的局部变量Args中,提取返回值到局部变量ReturnType中

    // 用Args数组添加Arg构造成新的参数类型,结合ReturnType构造成新的函数类型返回
    // 这样就完成了函数类型的修改
    type AppendArgumentResult = AppendArgument<(name:string)=>boolean,number>

    // 索引类型的重新构造

    // 索引类型是聚合多个元素的类型
    type objs = {
        name:string
        age:number
        gender:boolean
    }
    // 索引类型可以添加修饰符readonly(可读)、?(可选): 
    type objs1 = {
        readonly name:string
        age?:number
        gender:boolean
    }
    // 对它的修改和构造类型涉及到了映射类型的语法
    // type Mapping<Obj extends object> = {
    //     [Key in keyof Obj]: Obj[Key]
    // }

    // Mapping 映射的过程中可以对value做下修改 
    type Mapping<Obj extends object> = {
        [Key in keyof Obj]: [Obj[Key],Obj[Key],Obj[Key]]
    } 

    // 类型参数Obj是待处理的索引类型,通过extends约束为object
    // 用keyof取出Obj的索引,作为新的索引类型的索引,也就是 Key in keyof Obj

    // 值的类型可以做变换.这里我们用之前索引类型的值Obj[Key]构造成了
    // 三个元素的元组类型 [Obj[Key],Obj[Key],Obj[Key]]

    type res = Mapping<{a:1,b:2}>   //  {a:[1,1,1],b:[2,2,2]}
    // 索引类型的映射如下图所示
    // a:1 ====> a:[1,1,1]
    // b:2 ====> b:[2,2,2]

UppercaseKey

``ts

  • 比如把索引类型的Key变为大写
type UppercaseKey<Obj extends object> = {
        [Key in keyof Obj as Uppercase<Key & string>]: Obj[Key]
    }
    // 类型参数Obj是待处理的索引类型,通过extends约束为object

    // 新的索引类型的索引为Obj中的索引,也就是Key in keyof Obj 但要做些变换,也就是as之后

    // 通过Uppercase把索引Key转为大写,因为索引可能为string,number,symbol类型,而这里
    // 只能接受string类型,所以要 & string,也就是取索引中string的部分

    // value保持不变,也就是之前的索引Key对应的值的类型Obj[Key]
    // 这样构造出的新的索引类型,就把原来索引类型的索引转为了大写

    type UppercaseKeyResult = UppercaseKey<{ding:1,dong:2}>

Record ts提供了内置的高级类型Record来创建索引类型

``ts // type Record<K extends string | number | symbol, T> = { [P in K]: T}

// 指定索引和值的类型分别为K和T.就可以创建一个对应的索引类型

// 上面的索引类型的约束我们用的object 更语义化一点推荐用Record<string, object>

type UppercaseKeys<Obj extends Record<string,any>> = {
    [Key in keyof Obj as Uppercase<Key & string>] : Obj[Key]
}

// 也就是约束类型参数Obj为key为string,值为任意类型的索引类型

// ToReadonly 索引类型的索引可以添加readonly的修饰符,代表只读
type ToReadonly<T> = {
    readonly [Key in keyof T]: T[Key]
}
o 通过映射类型构造了新的索引类型,给索引加上了readonly的修饰,其余的保持不变
* 索引依然为原来的索引Key in keyof T, 值依然为原来的值T[Key]
```ts
    type ToReadonlyResult = ToReadonly<{name:string,age:number}>


    // ToPartial  同理,索引类型还可以添加可选修饰符

    type ToPartial<T> = {
        [Key in keyof T]?: T[Key]
    }
    // 给索引类型T的索引添加了?可选修饰符,其余保持不变
    type ToPartialResult = ToPartial<{name:string,age:number}>

    // ToMutable  可以添加readonly修饰,当然也可以去掉

    type ToMutable<T> = {
        -readonly[Key in keyof T]: T[Key]
    }

    type ToMutableResult = ToMutable<{readonly name:string,readonly age:number}>
    
    // ToRequired 同理 也可以去掉可选修饰符
    type ToRequired <T> = {
        [Key in keyof T]-?: T[Key]
    }

    type ToRequiredResult = ToRequired<{name?:string,age?:number}>

    // FilterByValueType  可以在构造新索引类型的做下过滤
    type FilterByValueType<Obj extends Record<string,any>
    ,ValueType> = {
        [Key in keyof Obj as ValueType extends
            Obj[Key] ? Key : never]
                : Obj[Key] 
    }

类型参数Obj为要处理的索引类型,通过extends约束为索引为string

  • 为任意类型的索引类型Record<string,any>

  • 类型参数ValueType为要过滤出的值的类型

  • 型参数ValueType为要过滤出的值的类型

  • 造新的索引类型,索引为Obj的索引,也就是Key in keyof Obj ,但

  • 做一些变换,也就是as之后的部分

  • 如果原来索引的值Obj[Key]是ValueType类型,索引依然为之前的索引Key *** 否则索引设置为never,never的索引会在生成新的索引类型时被去掉

  • 值保持不变,依然为原来索引的值,也就是Obj[Key]

    // 这样就达到了过滤索引类型的索引,产生新的索引类型的目的

    interface Person {
        name:string
        age:number
        hobby:string[]
    }

    type FilterResult = FilterByValueType<Person, string | number>

小结

  • Oridin,infer类型参数来保存任意类型,相当于变量的作用

  • 但其实也不能叫变量,因为他们是不可变的,想要变化就需要重新构造新的类型,并且可以

  • 在构造新类型的过程中对源类型做一些过滤和变换

  • 数组,字符串,函数,索引类型等都可以用这种方式对源类型做变换产生新的类型,其中

  • 索引类型有专门的语法叫做映射类型,对索引做修改的as叫做重映射

  • 提取和构造这两者是相辅相成的,很多类型体操都可以根据模式匹配做提取及重新构造做变换的套路

<================================================================================>

递归复用

  • 递归是吧问题分解为一系列相似的小问题,通过函数不断调用自身来解决这一个个小问题

  • 知道满足结束条件,就完成了问题的求解

  • TS的高级类型支持类型参数,可以做各种类型运算逻辑,返回新的类型,和函数调用时对应的

  • 自然也支持递归

  • TS类型系统不支持循环,但支持递归,当处理数量不固定的类型的时候,可以只处理一个类型

  • 然后递归的调用自身处理下一个类型,直到结束条件也就是所有的类型都处理完了,就完成了不

  • 确定数量的类型编程,达到循环的效果

Promise的递归调用

DeepPromiseValueType

  • 我们先实现一个提取不确定层数的Promise中的value的高级类型
type ttt = Promise<Promise<Promise<Record<string,any>>>>

// 这里是3层Promise,value类型是索引类型
type DeepPromiseValueType<P extends Promise<unknown>> =
    P extends Promise<infer ValueType>
        ? ValueType extends Promise<unknown>
            ? DeepPromiseValueType<ValueType>
            : ValueType
        : never
  • 类型参数P是待处理的Promise,通过extends约束为Promise类型.value类型不确定.设为unknown
  • 每次只处理一个类型的提取.也就是通过模式匹配提取出value的类型到infer声明额局部变量ValueType中
  • 然后判断如果ValueType依然是Promise类型,那就递归处理
  • 结束条件就是ValueType不为Promise类型,那就处理完了所有的层数,返回这时的ValueType
  • 这样,我们就提取到了最里层的Promise的value类型,也就是索引类型
type DeepPromiseResult = DeepPromiseValueType<Promise<Promise<Record<string,any>>>>

数组类型的递归

ReverseArr

  • 现有如下一个元祖类型
type arr = [1,2,3,4,5]
// 我们将其翻转就是 [5,4,3,2,1]
type ArrReverse<Arr extends unknown[]> = Arr extends
    [infer One,infer Two,infer Three,infer Four,infer Five]
    ? [Five,Four,Three,Two,One]
    : []
type ArrReverseResult = ArrReverse<arr>

// 如果我们的数组长度不确定,就需要用递归来处理
type resusionArr<Arr extends unknown[]> = Arr extends
    [infer First, ...infer ArrRest]
    ? [...resusionArr<ArrRest>,First]
    : Arr
type resusionArrResult = resusionArr<[1,2,3,4,5,6,7,8,9]>

Includes

  • 既然递归可以做循环用,那么查找元素这种自然也可以实现
  • 比如查找[1,2,3,4,5]中是否存在4,是就返回true,否则就返回false
type FindInArr<Arr extends unknown[],FindItem> = Arr extends
    [infer First, ...infer Rest] 
    ? IsEqual<First, FindItem> extends true
        ? true 
        : FindInArr<Rest, FindItem>
    : false

type IsEqual<A,B> = (A extends B ? true : false)
   & (B extends A ? true : false)

// 类型参数Arr是待查找的数组类型,元素类型任意,也就是unknown,FindItem待查找的元素类型

// 每次提取一个元素到infer声明的局部变量First中,剩余的放到局部变量Rest

// 判断First是否是要继续查找的元素,也就是和FindItem相等,是的话就返回true,否则递归判断下一个

// 直到结束条件也就是提取不出下一个元素,这时返回false

// 相等的判断就是A是B的子类型,并且B也是A的子类型,这就就完成了不确定长度的递归循环

type IncludesResult1 = FindInArr<[1,2,3,4,5],4>

type IncludesResult2 = FindInArr<[1,2,3,4,5],6>

RemoveItem

  • 可以查找自然就可以删除,只需要改下返回结果,构造一个新的数组返回
type RemoveInArr<
    Arr extends unknown[],
    RemoveItem,
    ResultArr extends unknown[] = []> = Arr extends
    [infer First, ...infer Rest]
    ? IsEqual2<First,RemoveItem> extends true
        ? RemoveInArr<Rest,RemoveItem,ResultArr>
        : RemoveInArr<Rest,RemoveItem,[...ResultArr, First]>
    : ResultArr

type IsEqual2<A,B> = (A extends B ? true : false) & (B extends A ? true : false)

// 类型参数Arr是待处理的数组,元素类型任意,也就是unknown[],类型参数Item为待查找的元素类型
// 类型参数ResultArr是构造出来的新数组,默认值是[]

// 通过模式匹配提取数组中的一个元素的类型,如果是Item类型的话就删除,
// 也就是不放入构造的新数组,直接返回之前的Result

// 否则放入构造的新数组,也就是再构造一个新的数组[...Result.First]
// 直到模式匹配不再满足,也就是处理完了所有的元素,返回这时候的Result

type RemoveInArrResult = RemoveInArr<[1,2,2,3,4,5],2>

BuildArray

  • 当我们构造数组的时候,也可以使用递归
  • 比如传入5和元素类型,构造一个长度为5的该元素类型构成的数组
type BuildArray <Length extends number,
     Ele extends unknown,
     Arr extends unknown[]> = Arr['length'] extends Length
     ? Arr 
     : BuildArray<Length,Ele,[...Arr, Ele]>
    
// 类型参数Length为数组长度,约束为number,类型参数Ele为元素类型 默认值为unknown
// 类型参数Arr为构造出的数组,默认值为[]

// 每次判断下Arr的长度是否到了Length,是的话就返回Arr,否则在Arr上加一个元素,然后递归构造

字符串类型的递归

ReplaceAll

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

type ReplaceAllResult = ReplaceAll<'xqw','x','w'>

// 但是这里只能处理一个字符的替换,如果要替换多个就要递归替换
type ReplaceAll2< Str extends string,
                  From extends string,
                  To extends string> = Str extends
                  `${infer Left}${From}${infer Right}`
                  ? `${Left}${To}${ReplaceAll2<Right,From,To>}`
                  : Str

    // 类型参数Str是待处理的字符串类型,From是待替换的字符,To是替换到的字符
    // 通过模式匹配提取From左右的字符串到infer声明的局部变量Left和Right里,

    // 用Left和To构造新的字符串,剩余的Right部分继续递归的替换

    // 结束条件是不再满足模式匹配,也就是没有要替换的元素,这是就直接返回字符串Str

    type ReplaceAll2Result = ReplaceAll2<'xxxxssss','s','w'> 

StringToUnion

  • 我们想把字符串字面量类型的每个字符都提取出来.组成来联合类型
  • 也即是把'ding'转为'd' | 'i' | 'n' | 'g'
    type StringToUnion <Str extends string> = Str extends
        `${infer One}${infer Two}${infer Three}${infer Four}`
        ? One|Two|Three|Four
        : never
    
    type StringToUnionResult = StringToUnion<'ding'>

    // 同样如果长度不确定,继续用到递归

    type StringToUnion2 <Str extends string> = Str extends 
                         `${infer First}${infer Rest}`
                         ? First | StringToUnion2<Rest>
                         : never
                         
    // 类型参数Str为待处理的字符串类型,通过extends约束为string

    // 通过模式匹配提取到第一个字符到infer声明的局部变量First.其余的
    // 字符放到局部变量Rest

    // 这样就完成了不确定长度的字符串的提取和联合类型的构造

    type StringToUnion2Result = StringToUnion2<'xdaegdsasd'>

ReverseString

  • 我们实现了数组的翻转,同样也可以实现字符串的翻转
    type ReverseStr <Str extends string,
                    Result extends string = ''> = Str extends
         `${infer First}${infer Rest}`
         ? ReverseStr<Rest, `${First}${Result}`>
         : Result
         
         
    // 类型参数Str为待处理的字符窜,类型参数Result为构造出的字符串,默认值是空串
    
    // 通过模式匹配提取到第一个字符到infer声明的局部变量First,其余字符放到Rest
    
    // 用First和之前的Result构造成新的字符串,吧First放到前面,因为递归是从左到右处理
    
    // 那么不断往前插入就相当于把右边的放到了左边,也就完成了字符翻转的功能
    
    type ReverseStrResult = ReverseStr<'wasd'>

DeepReadonly

  • 对线类型的递归,也叫做索引类型的递归
  • 之前我们有给对象加过readonly以及可选操作符
  • 现在如果这个索引类型的层数不确定该如何处理
    type obj = {
            a: {
                b:{
                    c:{
                        f:()=>'ding',
                        d:{
                            e:{
                                dong:string
                            }
                        }
                    }
                }
            }
    }

    type DeepReadonlyObj <Obj extends Record<string,any>> = 
    Obj extends any ?{
        readonly [Key in keyof Obj]: Obj[Key] extends object
        ? Obj[Key] extends Function
            ? Obj[Key]
            : DeepReadonlyObj<Obj[Key]>
        : Obj[Key]
    }
    : never

    // 类型参数Obj是待处理的索引类型,约束为Record<string,any>也就是索引string,值为任意类型

    // 索引映射自之前的索引,也就是Key in keyof Obj ,只不过加上了readonly的修饰

    // 值要做下判断,如果是object类型并且还是Function,那么久直接取之前的值Obj[Key]

    // 如果是object类型但不是Function,那就是说也是一个索引类型,
    // 就递归处理Deepreadonly<Obj[Key]>

    // 否则,值不是object就直接返回之前的值Obj[Key]

    type DeepReadonlyObjResult = DeepReadonlyObj<
            {a:
                {b:
                    {name:string,
                        c:
                        {
                            readonly age : number,
                            gender:number
                        }
                    }
                }
            }>

小结

  • 递归是把问题分解成一个个子问题,通过解决一个个子问题来解决整个问题

  • 结构就是不断的调用函数自身,直到达到约束的条件,终止递归

  • 在TS类型系统中的高级类型也同样支持递归,在类型体操中,遇到数量不确定的问题,要

  • 自然而然的想到用递归去处理,比如数组长度不确定,字符串长度不确定,索引类型层数不确定等 <===================================================================================>

  • TS 类型系统不是图灵完备.不是所有的逻辑都能写

  • 比如数值相关的逻辑

数组长度做计算

  • TS 类型系统没有加减乘除运算符,怎么做数值运算
  • 我们的数组类型取Length就是数值
type num1 = [unknown]['length']  // 1
type num2 = [unknown,unknown]['length']  // 2
  • 而数组类型我们是可以构建出来的,那么通过构造不同的数组区长度
  • 就能做到数值的运算
  • TS类型系统中没有加减乘除运算符,但是可以通过构造不同的数组然后
  • 取length的方式来完成数值计算,把数值的加减乘除转化为数组的提取和构造
  • (严格来说构造的是元组) 这点也是类型体操中最麻烦的一个点
  • 解下来实现我们的数组长度实现加减乘除

Add

  • 我们知道了数值计算要转换为对数组类型的操作,那么加法的实现很容易想到
  • 构造两个数组,然后合并成一个,再取Length
  • 构造的数组长度是不确定的,我们就需要使用递归构造
type BuildArr <Length extends number,
               Ele = unknown,
               Arr extends unknown[] = []> =
               Arr['length'] extends Length
               ? Arr 
               : BuildArr<Length,Ele,[...Arr,Ele]>

// 类型参数Length是要构造的数组的长度,类型参数Ele是数组元素
// 默认为unknown 类型参数Arr为构造出的数组,默认是[]

// 如果Arr的长度到达了Length,就返回构造出的Arr,否则继续递归构造
// 构造数组实现了,那么基于它就能实现加法 

type Add<Num1 extends number,Num2 extends number> = 
     [...BuildArr<Num1>,...BuildArr<Num2>]['length']

type AddResult = Add<223,22>

Substract

  • 加法是构造数组,那减法是从数值中去掉一部分
  • 我们可以通过数组类型的提取来实现
  • 比如3是[unknown,unknown,unknown]的数组类型,取出2个后就变成1
type Substract <Num1 extends number,
                Num2 extends number> = 
         BuildArr<Num1> extends 
         [...arr1: BuildArr<Num2>,...arr2: infer Rest]
         ? Rest['length']
         : never

type SubstractResult = Substract<25,22>

Multipy

  • 我们吧加法转换为了数组构造,把剪发转换为了数组提取,那乘法呢
  • 这里我们需要在加法的基础上多加一个参数,来传递中间结果的数组,算完之后再取一个length
type Multipy <Num1 extends number,
              Num2 extends number,
              ResultArr extends unknown[] = [] > = 
              Num2 extends 0 ? ResultArr['length']
              : Multipy<Num1, Substract<Num2,1>,
              [...BuildArr<Num1>,...ResultArr]>

// 类型参数Num1和Num2分别是被加数和加数

// 因为乘法是多个加法结果的累加,我们使用了一个类型参数ResultArr 来保存中间结果
// 默认值为[] 相当于从0开始

type MultipyRes = Multipy<2,33>

Divid

  • 乘法是递归的累加,那除法就是递归的累减
type Divid<Num1 extends number,
           Num2 extends number,
           CountArr extends unknown[] = [] > = 
           Num1 extends 0 ? CountArr['length']
           : Divid<Substract<Num1,Num2>,Num2,[unknown,...CountArr]>
     
// 类型参数Num1和Num2分别是被减数和减数
// 类型参数CountArr 是用来记录减了几次的累加数组
// 如果Num1减到了0,那么这时候减了几次就是除法结果.也就是CountArr['length']
type DividRes = Divid<20,4>

数组长度实现计数

Strlen

  • 数组长度可以取length得到,但是字符串类型不能取length
  • 字符串长度不确定.明显要用递归,每次娶一个计数,就是字符长度
type StrLen < Str extends string,
              CountArr extends unknown[] = [] > = 
              Str extends `${string}${infer Rest}`
              ? StrLen<Rest,[...CountArr,unknown]>
              : CountArr['length']

// 类型参数Str是待处理的字符串,类型参数CountArr是做计数的数组
// 默认值[]代表从0开始

// 每次通过模式匹配提取去掉一个字符之后的剩余字符串,并且往计数
// 数组里多放入一个元素,递归进行取字符和计数

type StrLenRes = StrLen<'Hello World'>

GreaterThan

  • 两个值的相互比较
type GreaterThan < Num1 extends number,
               Num2 extends number,
               CountArr extends unknown[] = [] > =
               Num1 extends Num2
               ? false
               : CountArr['length'] extends Num2
                     ? true
                     : CountArr['length'] extends Num1
                          ? false
                          : GreaterThan<Num1,Num2,[...CountArr,unknown]>

// 类型参数Num1和Num2是待比较的两个数

// 类型参数CountArr是计数用的,会不断累加.默认值[]代表从0开始

// 如果Num1 extends Num2 成立 代表相等 直接返回false

// 否则判断计数数组的长度,如果先到了Num2.那就是Num1大,返回true

type GreaterThanRes = GreaterThan<3,56>

来源:神光