TypeScript进阶二

高级类型(二)

上一篇介绍了高级进阶类型中未使用泛型的部分,现在我们来有关泛型的高级类型

1 索引类型

从对象中抽取一些属性的值,然后拼接成数组,可以这么写,

const userInfo = {
  name: 'lin',
  age: '18',
}

function getValues(userInfo: any, keys: string[]) {
  return keys.map(key => userInfo[key])
}

// 抽取指定属性的值
console.log(getValues(userInfo, ['name','age']))  // ['lin', '18']
// 抽取obj中没有的属性:
console.log(getValues(userInfo, ['sex','outlook']))  // [undefined, undefined]

虽然 obj 中并不包含 sex 和 outlook 属性,但 TS 编译器并未报错
此时使用 TS 索引类型,对这种情况做类型约束,实现动态属性的检查。
理解索引类型,需先理解 keyof(索引查询)、T[K](索引访问) 和 extends (泛型约束)。

1.1 keyof(索引查询)

keyof 操作符可以用于获取某种类型的所有键,其返回类型是联合类型。

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

type Test = keyof IPerson; // 'name' | 'age'

上面的例子,Test 类型变成了一个字符串字面量。

1.2 T[K](索引访问)

T[K],表示接口 T 的属性 K 所代表的类型,

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

let type1:  IPerson['name'] // string
let type2:  IPerson['age']  // number

1.3 extends (泛型约束)

T extends U,表示泛型变量可以通过继承某个类型,获得某些属性,之前讲过,复习一下,

interface ILength {
    length: number
}

function printLength<T extends ILength>(arg: T): T {
    console.log(arg.length)
    return arg
}

这样入参就一定要有 length 属性,比如 str、arr、obj 都可以, num 就不行。

const str = printLength('lin')
const arr = printLength([1,2,3])
const obj = printLength({ length: 10 })

const num = printLength(10) // 报错,Argument of type 'number' is not assignable to parameter of type 'ILength'

1.4 检查动态属性

对索引类型的几个概念了解后,对 getValue 函数进行改造,实现对象上动态属性的检查。

1)改造前,

const userInfo = {
  name: 'lin',
  age: '18',
}

function getValues(userInfo: any, keys: string[]) {
  return keys.map(key => userInfo[key])
}

● 定义泛型 T、K,用于约束 userInfo 和 keys
● 为 K 增加一个泛型约束,使 K 继承 userInfo 的所有属性的联合类型, 即K extends keyof T

2) 改造后,

function getValues<T, K extends keyof T>(userInfo: T, keys: K[]): T[K][] {
    return keys.map(key => userInfo[key])
}

这样当我们指定不在对象里的属性时,就会报错

2 映射类型

TS允许将一个类型映射成另外一个类型。

  • in

介绍映射类型之前,先介绍一下 in 操作符,用来对联合类型实现遍历。

type Person = "name" | "school" | "major"

type Obj =  {
  [p in Person]: string
}

2.1 Partial

Partial将T的所有属性映射为可选的,例如:

interface IPerson {
    name: string
    age: number
}

let p1: IPerson = {
    name: 'lin',
    age: 18
}

使用了 IPerson 接口,就一定要传 name 和 age 属性

使用 Partial 改造一下,就可以变成可选属性,

interface IPerson {
    name: string
    age: number
}

type IPartial = Partial<IPerson>

let p1: IPartial = {}

Partial 原理
Partial 的实现用到了 in 和 keyof

/**
 * Make all properties in T optional
 */
type Partial<T> = {
    [P in keyof T]?: T[P]
}

● [P in keyof T]遍历T上的所有属性
● ?:设置为属性为可选的
● T[P]设置类型为原来的类型

2.2 Readonly

Readonly将T的所有属性映射为只读的,例如:

interface IPerson {
  name: string
  age: number
}

type IReadOnly = Readonly<IPerson>

let p1: IReadOnly = {
  name: 'lin',
  age: 18
}

Readonly 原理
和 Partial 几乎完全一样,

/**
 * Make all properties in T readonly
 */
type Readonly<T> = {
    readonly [P in keyof T]: T[P]
}

● [P in keyof T]遍历T上的所有属性
● readonly设置为属性为可选的
● T[P]设置类型为原来的类型

2.3 Pick

Pick用于抽取对象子集,挑选一组属性并组成一个新的类型,例如:

interface IPerson {
  name: string
  age: number
  sex: string
}

type IPick = Pick<IPerson, 'name' | 'age'>


let p1: IPick = {
  name: 'lin',
  age: 18
}

这样就把 name 和 age 从 IPerson 中抽取出来。

Pick 原理

/**
 * From T, pick a set of properties whose keys are in the union K
 */
type Pick<T, K extends keyof T> = {
    [P in K]: T[P]
}

Pick映射类型有两个参数:
● 第一个参数T,表示要抽取的目标对象
● 第二个参数K,具有一个约束:K一定要来自T所有属性字面量的联合类型

2.4 Record

上面三种映射类型官方称为同态,意思是只作用于 obj 属性而不会引入新的属性。
Record 是会创建新属性的非同态映射类型。

interface IPerson {
  name: string
  age: number
}

type IRecord = Record<string, IPerson>

let personMap: IRecord = {
   person1: {
       name: 'lin',
       age: 18
   },
   person2: {
       name: 'liu',
       age: 25
   } 
}

Record 原理

/**
 * Construct a type with a set of properties K of type T
 */
type Record<K extends keyof any, T> = {
    [P in K]: T
}

Record 映射类型有两个参数:
● 第一个参数可以传入继承于 any 的任何值
● 第二个参数,作为新创建对象的值,被传入。

2.5 条件类型

T extends U ? X : Y 

//若类型 T 可被赋值给类型 U,那么结果类型就是 X 类型,否则就是 Y 类型

Exclude 和 Extract 的实现就用到了条件类型。

2.5.1 Exclude

Exclude 意思是不包含,Exclude<T, U> 会返回 联合类型 T 中不包含 联合类型 U 的部分。

type Test = Exclude<'a' | 'b' | 'c', 'a'>

Exclude 原理

/**
 * Exclude from T those types that are assignable to U
 */
type Exclude<T, U> = T extends U ? never : T

● never表示一个不存在的类型
● never与其他类型的联合后,为其他类型

type Test = string | number | never  
2.5.2 Extract

Extract<T, U>提取联合类型 T 和联合类型 U 的所有交集。

type Test = Extract<'key1' | 'key2', 'key1'>

Extract 原理

/**
 * Extract from T those types that are assignable to U
 */
type Extract<T, U> = T extends U ? T : never

懂了 Exclude,也就懂了 Extract。

2.6 工具类型(Utility Types)

为了方便开发者使用, TypeScript 内置了一些常用的工具类型。
上文介绍的索引类型、映射类型和条件类型都是工具类型。
除了上文介绍的,再介绍一些常用的,毕竟工具函数遇到了去查就行,死记硬背就太枯燥了,熟能生巧,写多了自然就熟悉了。

2.6.1 Omit

Omit<T, U>从类型 T 中剔除 U 中的所有属性。

interface IPerson {
    name: string
    age: number
}

type IOmit = Omit<IPerson, 'age'>

这样就剔除了 IPerson 上的 age 属性。

Omit 原理

/**
 * Construct a type with the properties of T except for those in type K.
 */
type Omit<T, K extends keyof any> = 
Pick<T, Exclude<keyof T, K>>

Pick用于挑选一组属性并组成一个新的类型,Omit 是剔除一些属性,留下剩余的,他们俩有点相反的感觉。
那么就可以用 Pick 和 Exclude 实现 Omit。
当然也可以不用 Pick 实现,

type Omit2<T, K extends keyof any> = {
  [P in Exclude<keyof T, K>]: T[P]
}
2.6.2 NonNullable

NonNullable 用来过滤类型中的 null 及 undefined 类型。

type T0 = NonNullable<string | number | undefined>; // string | number
type T1 = NonNullable<string[] | null | undefined>; // string[]

NonNullable 原理

/**
 * Exclude null and undefined from T
 */
type NonNullable<T> = 
T extends null | undefined ? never : T

● never表示一个不存在的类型
● never与其他类型的联合后,为其他类型

2.6.3 Parameters

Parameters 获取函数的参数类型,将每个参数类型放在一个元组中。

type T1 = Parameters<() => string>  // []

type T2 = Parameters<(arg: string) => void>  // [string]

type T3 = Parameters<(arg1: string, arg2: number) => void> // [arg1: string, arg2: number]

Parameters 原理

/**
 * Obtain the parameters of a function type in a tuple
 */
type Parameters<T extends (...args: any) => any> = 
T extends (...args: infer P) => any ? P : never

在条件类型语句中,可以用 infer 声明一个类型变量并且对它进行使用。
● Parameters首先约束参数T必须是个函数类型
● 判断T是否是函数类型,如果是则使用infer P暂时存一下函数的参数类型,后面的语句直接用 P 即可得到这个类型并返回,否则就返回never

2.6.4 ReturnType

ReturnType 获取函数的返回值类型。

type T0 = ReturnType<() => string>  // string

type T1 = ReturnType<(s: string) => void>  // void

ReturnType 原理

/**
 * Obtain the return type of a function type
 */
type ReturnType<T extends (...args: any) => any> = 
T extends (...args: any) => infer R ? R : any

懂了 Parameters,也就懂了 ReturnType,
● ReturnType首先约束参数T必须是个函数类型
● 判断T是否是函数类型,如果是则使用infer R暂时存一下函数的返回值类型,后面的语句直接用 R 即可得到这个类型并返回,否则就返回any

2.6.5 类型体操

在本节中,我们熟悉了很多工具类型的作用和原理,其实已经在不知不觉中做了一些类型体操了
TypeScript 高级类型会根据类型参数求出新的类型,这个过程会涉及一系列的类型计算逻辑,这些类型计算逻辑就叫做类型体操。当然,这并不是一个正式的概念,只是社区的戏称,因为有的类型计算逻辑是比较复杂的。
想一想我们之前研究的这些工具类型,都是在对类型做计算返回新的类型啊。
Ts是一门图灵完备的编程语言,即类型的可编码化,可以通过代码逻辑生成指定的各种类型,基于这点,才会有各种类型体操。
个人觉得 TS 类型体操这种东西,我们这些搬砖人不必像刷算法一样刻意去训练,浅尝辄止,了解即可,更多可了解 这篇文章[10]。

3 TS 声明文件

3.1 declare

当使用第三方库时,很多三方库不是用 TS 写的,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。
比如,在 TS 中直接使用 Vue,就会报错,

const app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

这时,我们可以使用 declare 关键字来定义 Vue 的类型,简单写一个模拟一下,

interface VueOption {
    el: string,
    data: any
}

declare class Vue {
    options: VueOption
    constructor(options: VueOption)
}

const app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

这样就不会报错了,使用 declare 关键字,相当于告诉 TS 编译器,这个变量(Vue)的类型已经在其他地方定义了,你直接拿去用,别报错。
需要注意的是,declare class Vue 并没有真的定义一个类,只是定义了类 Vue 的类型,仅仅会用于编译时的检查,在编译结果中会被删除。它编译结果是:

const app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

3.2 .d.ts

通常我们会把声明语句放到一个单独的文件(Vue.d.ts)中,这就是声明文件,以 .d.ts 为后缀。

// src/Vue.d.ts

interface VueOption {
    el: string,
    data: any
}

declare class Vue {
    options: VueOption
    constructor(options: VueOption)
}
// src/index.ts

const app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

一般来说,ts 会解析项目中所有的 *.ts 文件,当然也包含以 .d.ts 结尾的文件。所以当我们将 Vue.d.ts 放到项目中时,其他所有 *.ts 文件就都可以获得 Vue 的类型定义了。

/path/to/project
├── src
|  ├── index.ts
|  └── Vue.d.ts
└── tsconfig.json

3.3 使用三方库

那么当我们使用三方库的时候,是不是所有的三方库都要写一大堆 decare 的文件呢?
答案是不一定,要看社区里有没有这个三方库的 TS 类型包(一般都有)。
社区使用 @types 统一管理第三方库的声明文件,是由 DefinitelyTyped[11] 这个组织统一管理的
比如安装 lodash 的类型包,

npm install @types/lodash -D

只需要安装了,就可以在 TS 里正常使用 lodash 了,别的啥也不用做。

当然,如果一个库本来就是 TS 写的,就不用担心类型文件的问题,比如 Vue3。

3.4 自己写声明文件

比如你以前写了一个请求小模块 myFetch,代码如下,

function myFetch(url, method, data) {
    return fetch(url, {
        body: data ? JSON.stringify(data) : '',
        method
    }).then(res => res.json())
}

myFetch.get = (url) => {
    return myFetch(url, 'GET')
}

myFetch.post = (url, data) => {
    return myFetch(url, 'POST', data)
}

export default myFetch

现在新项目用了 TS 了,要在新项目中继续用这个 myFetch,你有两种选择:
● 用 TS 重写 myFetch,新项目引重写的 myFetch
● 直接引 myFetch ,给它写声明文件
如果选择第二种方案,就可以这么做,

type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'

declare function myFetch<T = any>(url: string, method: HTTPMethod, data?: any): Promise<T>

declare namespace myFetch { // 使用 namespace 来声明对象下的属性和方法
    const get: <T = any>(url: string) => Promise<T> 
    const post: <T = any>(url: string, data: any) => Promise<T>
}

比较麻烦的是需要配置才行,可以有两种选择,

  1. 创建一个 node_modules/@types/myFetch/index.d.ts 文件,存放 myFetch 模块的声明文件。这种方式不需要额外的配置,但是 node_modules 目录不稳定,代码也没有被保存到仓库中,无法回溯版本,有不小心被删除的风险,故不太建议用这种方案,一般只用作临时测试。

  2. 创建一个 types 目录,专门用来管理自己写的声明文件,将 myFetch 的声明文件放到 types/myFetch/index.d.ts 中。这种方式需要配置下 tsconfig.json 中的 pathsbaseUrl 字段。

// tsconfig.json
{
    "compilerOptions": {
        "module": "commonjs",
        "baseUrl": "./",
        "paths": {
            "*": ["types/*"]
        }
    }
}

感觉直接用 TS 重写比给老项目写声明文件更好,这样就不用专门维护类型模块了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值