关于typescript:类型体操的9种类型运算4种类型套路总结

30次阅读

共计 8635 个字符,预计需要花费 22 分钟才能阅读完成。

明天给大家分享的主题是一起来做类型体操。

次要分为 4 个局部进行介绍:

  1. 类型体操的背景,通过背景理解为什么要在我的项目中退出类型体操;
  2. 理解类型体操的次要类型、运算逻辑、和类型套路;
  3. 类型体操实际,解析 TypeScript 内置高级类型,手写 ParseQueryString 简单类型;
  4. 小结,综上分享,积淀论断。

一、背景

在背景章节介绍的是什么是类型,什么是类型平安,怎么实现类型平安,什么是类型体操?

以理解类型体操的意义。

1. 什么是类型?

理解什么是类型之前,先来介绍两个概念:

  • 不同类型变量占据的内存大小不同

boolean 类型的变量会调配 4 个字节的内存,而 number 类型的变量则会调配 8 个字节的内存,给变量申明了不同的类型就代表了会占据不同的内存空间。

  • 不同类型变量可做的操作不同

number 类型能够做加减乘除等运算,boolean 就不能够,复合类型中不同类型的对象可用的办法不同,比方 Date 和 RegExp,变量的类型不同代表能够对该变量做的操作就不同。

综上,能够失去一个简略的论断就是,类型就是编程语言提供对不同内容的形象定义

2. 什么是类型平安?

理解了类型的概念后,那么,什么是类型平安呢?

一个简略的定义就是,类型平安就是只做该类型容许的操作。比方对于 boolean 类型,不容许加减乘除运算,只容许赋值 true、false。

当咱们能做到类型平安时,能够大量的缩小代码中潜在的问题,大量进步代码品质。

3. 怎么实现类型平安?

那么,怎么做到类型平安?

这里介绍两种类型查看机制,别离是动静类型检查和动态类型查看。

3.1 动静类型查看

Javascript 就是典型的动静类型查看,它在编译时,没有类型信息,到运行时才查看,导致很多暗藏 bug。

3.2 动态类型查看

TypeScript 作为 Javascript 的超集,采纳的是动态类型查看,在编译时就有类型信息,查看类型问题,缩小运行时的潜在问题。

4. 什么是类型体操

下面介绍了类型的一些定义,都是大家相熟的一些对于类型的背景介绍,这一章节回归到本次分享的主题概念,类型体操。

理解类型体操前,先介绍 3 种类型零碎。

4.1 简略类型零碎

简略类型零碎,它只基于申明的类型做查看,比方一个加法函数,能够加整数也能够加小数,但在简略类型零碎中,须要申明 2 个函数来做这件事件。

int add(int a, int b) {return a + b}

double add(double a, double b) {return a + b}

4.2 泛型类型零碎

泛型类型零碎,它反对类型参数,通过给参数传参,能够动静定义类型,让类型更加灵便。

T add<T>(T a, T b) {return a + b}

add(1, 2)
add(1.1, 2.2)

然而在一些须要类型参数逻辑运算的场景就不实用了,比方一个返回对象某个属性值的函数类型。

function getPropValue<T>(obj: T, key) {return obj[key]
}

4.3 类型编程零碎

类型编程零碎,它不仅反对类型参数,还能给类型参数做各种逻辑运算,比方下面提到的返回对象某个属性值的函数类型,能够通过 keyof、T[K] 来逻辑运算失去函数类型。

function getPropValue<
  T extends object, 
  Key extends keyof T
>(obj: T, key: Key): T[Key] {return obj[key]
}

总结上述,类型体操就是类型编程,对类型参数做各种逻辑运算,以产生新的类型

之所以称之为体操,是因为它的复杂度,右侧是一个解析参数的函数类型,外面用到了很多简单的逻辑运算,等先介绍了类型编程的运算办法后,再来解析这个类型的实现。

二、理解类型体操

相熟完类型体操的概念后,再来持续理解类型体操有哪些类型,反对哪些运算逻辑,有哪些运算套路。

1. 有哪些类型

类型体操的次要类型列举在图中。TypeScript 复用了 JS 的根底类型和复合类型,并新增元组(Tuple)、接口(Interface)、枚举(Enum)等类型,这些类型在日常开发过程中类型申明应该都很罕用,不做赘述。

// 元组(Tuple)就是元素个数和类型固定的数组类型
type Tuple = [number, string];

// 接口(Interface)能够形容函数、对象、结构器的构造:interface IPerson {
    name: string;
    age: number;
}

class Person implements IPerson {
    name: string;
    age: number;
}

const obj: IPerson = {
    name: 'aa',
    age: 18
}

// 枚举(Enum)是一系列值的复合:enum Transpiler {
    Babel = 'babel',
    Postcss = 'postcss',
    Terser = 'terser',
    Prettier = 'prettier',
    TypeScriptCompiler = 'tsc'
}

const transpiler = Transpiler.TypeScriptCompiler;

2. 运算逻辑

重点介绍的是类型编程反对的运算逻辑。

TypeScript 反对条件、推导、联结、穿插、对联结类型做映射等 9 种运算逻辑。

  • 条件:T extends U ? X : Y

条件判断和 js 逻辑雷同,都是如果满足条件就返回 a 否则返回 b。

// 条件:extends ? :
// 如果 T 是 2 的子类型,那么类型是 true,否则类型是 false。type isTwo<T> = T extends 2 ? true : false;
// false
type res = isTwo<1>;
  • 束缚:extends

通过束缚语法 extends 限度类型。

// 通过 T extends Length 束缚了 T 的类型,必须是蕴含 length 属性,且 length 的类型必须是 number。interface Length {length: number}

function fn1<T extends Length>(arg: T): number{return arg.length}
  • 推导:infer

推导则是相似 js 的正则匹配,都满足公式条件时,能够提取公式中的变量,间接返回或者再次加工都能够。

// 推导:infer
// 提取元组类型的第一个元素:// extends 束缚类型参数只能是数组类型,因为不晓得数组元素的具体类型,所以用 unknown。// extends 判断类型参数 T 是不是 [infer F, ...infer R] 的子类型,如果是就返回 F 变量,如果不是就不返回
type First<T extends unknown[]> = T extends [infer F, ...infer R] ? F : never;
// 1
type res2 = First<[1, 2, 3]>;
  • 联结:|

联结代表能够是几个类型之一。

type Union = 1 | 2 | 3
  • 穿插:&

穿插代表对类型做合并。

type ObjType = {a: number} & {c: boolean}
  • 索引查问:keyof T

keyof 用于获取某种类型的所有键,其返回值是联结类型。

// const a: 'name' | 'age' = 'name'
const a: keyof {
    name: string,
    age: number
} = 'name'
  • 索引拜访:T[K]

T[K] 用于拜访索引,失去索引对应的值的联结类型。

interface I3 {
  name: string,
  age: number
}

type T6 = I3[keyof I3] // string | number
  • 索引遍历:in

in 用于遍历联结类型。

const obj = {
    name: 'tj',
    age: 11
}

type T5 = {[P in keyof typeof obj]: any
}

/*
{
  name: any,
  age: any
}
*/
  • 索引重映射:as

as 用于批改映射类型的 key。

// 通过索引查问 keyof,索引拜访 t[k],索引遍历 in,索引重映射 as,返回全新的 key、value 形成的新的映射类型
type MapType<T> = {
    [
    Key in keyof T
    as `${Key & string}${Key & string}${Key & string}`
    ]: [T[Key], T[Key], T[Key]]
}
// {//     aaa: [1, 1, 1];
//     bbb: [2, 2, 2];
// }
type res3 = MapType<{a: 1, b: 2}>

3. 运算套路

依据下面介绍的 9 种运算逻辑,我总结了 4 个类型套路。

  • 模式匹配做提取;
  • 从新结构做变换;
  • 递归复用做循环;
  • 数组长度做计数。

3.1 模式匹配做提取

第一个类型套路是模式匹配做提取。

模式匹配做提取的意思是通过类型 extends 一个模式类型,把须要提取的局部放到通过 infer 申明的局部变量里。

举个例子,用模式匹配提取函数参数类型。

type GetParameters<Func extends Function> =
    Func extends (...args: infer Args) => unknown ? Args : never;

type ParametersResult = GetParameters<(name: string, age: number) => string>

首先用 extends 限度类型参数必须是 Function 类型。

而后用 extends 为 参数类型匹配公式,当满足公式时,提取公式中的变量 Args。

实现函数参数类型的提取。

3.2 从新结构做变换

第二个类型套路是从新结构做变换。

从新结构做变换的意思是想要变动就须要从新结构新的类型,并且能够在结构新类型的过程中对原类型做一些过滤和变换。

比方实现一个字符串类型的从新结构。

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

type CapitalizeResult = CapitalizeStr<'tang'>

首先限度参数类型必须是字符串类型。

而后用 extends 为参数类型匹配公式,提取公式中的变量 First Rest,并通过 Uppercase 封装。

实现了首字母大写的字符串字面量类型。

3.3 递归复用做循环

第三个类型套路是递归复用做循环。

TypeScript 自身不反对循环,然而能够通过递归实现不确定数量的类型编程,达到循环的成果。

比方通过递归实现数组类型反转。

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


type ReverseArrResult = ReverseArr<[1, 2, 3, 4, 5]>

首先限度参数必须是数组类型。

而后用 extends 匹配公式,如果满足条件,则调用本身,否则间接返回。

实现了一个数组反转类型。

3.4 数组长度做计数

第四个类型套路是数组长度做计数。

类型编程自身是不反对做加减乘除运算的,然而能够通过递归结构指定长度的数组,而后取数组长度的形式来实现数值的加减乘除。

比方通过数组长度实现类型编程的加法运算。

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

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


type AddResult = Add<32, 25>

首先通过递归创立一个能够生成任意长度的数组类型

而后创立一个加法类型,通过数组的长度来实现加法运算。

三、类型体操实际

分享的第三局部是类型体操实际。

后面分享了类型体操的概念及罕用的运算逻辑。

上面咱们就用这些运算逻辑来解析 TypeScript 内置的高级类型。

1. 解析 TypeScript 内置高级类型

  • partial 把索引变为可选

通过 in 操作符遍历索引,为所有索引增加?前缀实现把索引变为可选的新的映射类型。

type TPartial<T> = {[P in keyof T]?: T[P];
};

type PartialRes = TPartial<{name: 'aa', age: 18}>
  • Required 把索引变为必选

通过 in 操作符遍历索引,为所有索引删除?前缀实现把索引变为必选的新的映射类型。

type TRequired<T> = {[P in keyof T]-?: T[P]
}

type RequiredRes = TRequired<{name?: 'aa', age?: 18}>
  • Readonly 把索引变为只读

通过 in 操作符遍历索引,为所有索引增加 readonly 前缀实现把索引变为只读的新的映射类型。

type TReadonly<T> = {readonly [P in keyof T]: T[P]
}

type ReadonlyRes = TReadonly<{name?: 'aa', age?: 18}>
  • Pick 保留过滤索引

首先限度第二个参数必须是对象的 key 值,而后通过 in 操作符遍历第二个参数,生成新的映射类型实现。

type TPick<T, K extends keyof T> = {[P in K]: T[P]
}

type PickRes = TPick<{name?: 'aa', age?: 18}, 'name'>
  • Record 创立映射类型

通过 in 操作符遍历联结类型 K,创立新的映射类型。

type TRecord<K extends keyof any, T> = {[P in K]: T
}

type RecordRes = TRecord<'aa' | 'bb', string>
  • Exclude 删除联结类型的一部分

通过 extends 操作符,判断参数 1 是否赋值给参数 2,如果能够则返回 never,以此删除联结类型的一部分。

type TExclude<T, U> = T extends U ? never : T

type ExcludeRes = TExclude<'aa' | 'bb', 'aa'>
  • Extract 保留联结类型的一部分

和 Exclude 逻辑相同,判断参数 1 是否赋值给参数 2,如果不能够则返回 never,以此保留联结类型的一部分。

type TExtract<T, U> = T extends U ? T : never

type ExtractRes = TExtract<'aa' | 'bb', 'aa'>
  • Omit 删除过滤索引

通过高级类型 Pick、Exclude 组合,删除过滤索引。

type TOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

type OmitRes = TOmit<{name: 'aa', age: 18}, 'name'>
  • Awaited 用于获取 Promise 的 valueType

通过递归来获取未知层级的 Promise 的 value 类型。

type TAwaited<T> =
    T extends null | undefined
        ? T
        : T extends object & {then(onfulfilled: infer F): any }
            ? F extends ((value: infer V, ...args: any) => any)
                ? Awaited<V>
                : never
            : T;


type AwaitedRes = TAwaited<Promise<Promise<Promise<string>>>>

还有十分多高级类型,实现思路和下面介绍的类型套路大多统一,这里不一一赘述。

2. 解析 ParseQueryString 简单类型

重点解析的是在背景章节介绍类型体操复杂度,举例说明的解析字符串参数的函数类型。

如图示 demo 所示,这个函数是用于将指定字符串格局解析为对象格局。

function parseQueryString1(queryStr) {if (!queryStr || !queryStr.length) {return {}
  }
  const queryObj = {}
  const items = queryStr.split('&')
  items.forEach((item) => {const [key, value] = item.split('=')
    if (queryObj[key]) {if (Array.isArray(queryObj[key])) {queryObj[key].push(value)
      } else {queryObj[key] = [queryObj[key], value]
      }
    } else {queryObj[key] = value
    }
  })
  return queryObj
}

比方获取字符串 a=1&b=2 中 a 的值。

罕用的类型申明形式如下图所示:

function parseQueryString1(queryStr: string): Record<string, any> {if (!queryStr || !queryStr.length) {return {}
  }
  const queryObj = {}
  const items = queryStr.split('&')
  items.forEach((item) => {const [key, value] = item.split('=')
    if (queryObj[key]) {if (Array.isArray(queryObj[key])) {queryObj[key].push(value)
      } else {queryObj[key] = [queryObj[key], value]
      }
    } else {queryObj[key] = value
    }
  })
  return queryObj
}

参数类型为 string,返回类型为 Record<string, any>,这时看到,res1.a 类型为 any,那么有没有方法,精确的晓得 a 的类型是 字面量类型 1 呢?

上面就通过类型体操的形式,来重写解析字符串参数的函数类型。

首先限度参数类型是 string 类型,而后为参数匹配公式 a&b,如果满足公式,将 a 解析为 key value 的映射类型,将 b 递归 ParseQueryString 类型,持续解析,直到不再满足 a&b 公式。

最初,就能够失去一个精准的函数返回类型,res.a = 1


type ParseParam<Param extends string> =
    Param extends `${infer Key}=${infer Value}`
        ? {[K in Key]: Value
        } : Record<string, any>;

type MergeParams<
    OneParam extends Record<string, any>,
    OtherParam extends Record<string, any>
> = {readonly [Key in keyof OneParam | keyof OtherParam]:
    Key extends keyof OneParam
        ? OneParam[Key]
        : Key extends keyof OtherParam
            ? OtherParam[Key]
            : never
}

type ParseQueryString<Str extends string> =
    Str extends `${infer Param}&${infer Rest}`
        ? MergeParams<ParseParam<Param>, ParseQueryString<Rest>>
        : ParseParam<Str>;
function parseQueryString<Str extends string>(queryStr: Str): ParseQueryString<Str> {if (!queryStr || !queryStr.length) {return {} as any;
    }
    const queryObj = {} as any;
    const items = queryStr.split('&');
    items.forEach(item => {const [key, value] = item.split('=');
        if (queryObj[key]) {if(Array.isArray(queryObj[key])) {queryObj[key].push(value);
            } else {queryObj[key] = [queryObj[key], value]
            }
        } else {queryObj[key] = value;
        }
    });
    return queryObj as any;
}


const res = parseQueryString('a=1&b=2&c=3');

console.log(res.a) // type 1

四、小结

综上分享,从 3 个方面介绍了类型体操。

  • 第一点是类型体操背景,理解了什么是类型,什么是类型平安,怎么实现类型平安;
  • 第二点是相熟类型体操的次要类型、反对的逻辑运算,并总结了 4 个类型套路;
  • 第三点是类型体操实际,解析了 TypeScript 内置高级类型的实现,并手写了一些简单函数类型。

从中咱们理解到须要动静生成类型的场景,必然是要用类型编程做一些运算,即便有的场景下能够不必类型编程,然而应用类型编程可能有更精准的类型提醒和查看,缩小代码中潜在的问题。

参考资料 + 源码

这里列举了本次分享的参考资料及示例源码,欢送大家扩大浏览。

  • 参考资料:《TypeScript 类型体操通关秘籍》
  • 示例源码:https://github.com/jiaozitang…

正文完
 0