TS系列——高级类型

467 阅读7分钟

我以为我和别人不同,最后才发现我还不如别人。

1.前言

距离上一篇文章又过去了一段时间啦,最近又偷懒了~😭。上一次梳理了一下TS中的基础类型,我们今天来看看TS中的高级类型有哪些? image.png

2.高级类型

字面量类型

在TypeScript中,我们可以直接用字面量来表示类型。字面量类型包含了以下三种:字符串字面量类型数字字面量类型布尔字面量类型

const str: 'Yancy' = 'Yancy';
const num: 999 = 999;
const bool: true = true;

通常情况下,我们都不会单独使用字面量类型,而是结合联合类型一起使用。例如我们在约束接口返回值时,通常我们的code和success都是固定的一些值。

interface ResponseData {
   code: 500200 | 500300 | 500400 | 500500;
   success: true | false
   data: any;
}

联合类型

联合类型代表了一组类型的可用集合,它使用|分隔每一个类型。

type Code = 500200 | 500300 | 500400 | 500500;
type Status = true | false;

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

interface IMan {
    name: string;
    sex: string;
}

type People = IPerson | IMan

注意:当TypeScript不确定一个联合类型的变量具体是其中的哪一个类型时,我们只能够访问该联合类型中各个子类型共有的属性和方法

function getLen(val: string | number): number {
    return val.length; // 报错,因为类型number上不存在属性length
}

// toString方法,不管是字符串还是number都存在
function getStr(val: string | number): string {
    return val.toString();
}

被声明为联合类型的变量,在最终被赋值时,TS会根据类型推论的规则推断出一个具体的类型:

let name: string | number;
name = 'Yancy' //这里则推断为string类型

交叉类型

交叉类型与联合类型刚好相反,它使用&分隔每一个类型,代表了按位与,即A & B需要同时满足A和B两个类型才行,而联合类型只需要满足其中一个即可。

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

type IStudent = IPerson & { grade: number}

交叉类型与interface的extends非常相似,都是为了进行组合和扩展。

索引类型

索引类型指的不是某一个特定的类型工具,而是包含了三个部分的内容:索引签名类型索引类型查询索引类型访问。它们唯一的共同点就是都是通过索引的形式来进行类型操作,但索引签名类型是声明,而索引类型查询和访问则是读取

索引签名类型

索引签名类型可以让我们快速声明一个键值类型一致的类型结构。例如,当我们无法确定一个对象中有哪些属性,或者说对象中可以出现任意多个属性时,我们就可以用到索引类型签名

interface IAnyObject {
    [key: string]: string;
}

在javascript中,通过obj[prop]形式来访问某一个属性值时,默认会将数字索引转换为字符串索引来访问,因此在字符串索引类型中我们也可以声明数字类型的键。

const userInfo: IAnyObject = {
    name: 'Yancy',
    123: '123',
}

索引类型签名也可以和具体的键值对类型声明并存,但必须保证具体的键值对类型符合索引签名类型的声明

interface IStringOrNumber {
    propA: number;
    propB: string;
    [key: string]: number | string;
}

索引类型查询

索引类型查询,也就是keyof操作符,它可以将对象中的所有键转换为对应字面量类型,然后再组合成联合类型。注意,这里并不会将数字类型的键名转换为字符串类型字面量,而是仍然保持为数字类型字面量。

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

type IPersonKeys = keyof IPerson; // name | age

keyof的产物必定是一个联合类型。

索引类型访问

在JavaScript中,我们可以通过obj[expression]的方式来动态访问一个对象属性,expression表达式会被先执行,然后使用返回值来访问属性。

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

type IName = IPerson['name']; // string
type IAge = IPerson['age']; // number

索引类型访问的本质就是通过键的字面量类型访问这个键所对应的键值类型。这里只是获取了单个键的类型,我们还可以配合keyof操作符,一次性获取某个对象中所有键对应的字面量类型

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

type IPropUnion = IPerson[keyof IPerson]; // string | number

映射类型

映射类型的主要作用是基于键名映射到键值类型

type Stringify<T> = {
    [K in keyof T]: string;
}

假设我们传入一个对象类型,那么通过keyof关键字获取到了这个对象类型的键名所组合而成的字面量联合类型,然后通过映射类型(in关键字)将这个联合类型的每一个成员映射出来,并将其键值类型设置为string。in关键字类似于JavaScript中数组的map方法。
有的情况下,我们可以需要将一个现有的类型的每个属性全部变为可选类型或者是只读类型的。

interface IPerson {
    name: string;
    age: number 
}

// 将每个属性都变为可选的
interface IPersonPartial {
    name?: string;
    age?: string;
}

// 将每个属性都变为只读的
interface IPersonPartial {
    readonly name: string;
    readonly age: string;
}

如果我们使用映射类型,可以完全简化上面的转化过程。

// 转化为可选类型
type Partial<T> = {
    [K in keyof T]?: T[K];
}

// 转化为只读类型
type Readonly<T> = {
    readonly [K in keyof T]: T[K];
}

这里的T[K]就是上面所提到的索引类型访问,使用键的字面量类型访问到了键值的类型。而K in属于映射类型的语法,keyof T属于keyof操作符,[K in keyof T][]属于索引签名类型,T[K]属于索引类型访问。

3. 泛型

泛型,非常类似于javascript函数中的参数。

type Factory<T> = T | number | string;

泛型约束与默认值

我们可以像给函数参数设置默认值一样,泛型也可以设置默认值。

type Factory<T = boolean> = T | number | string;

泛型约束,其实也就说我们传入的泛型必须满足某些条件,否则就无法进行继续执行。在JavaScript中我们通常会使用if语句来判断是否满足某个条件,不满足的话直接return或者抛出错误。而在泛型中,我们可以使用extends关键字来约束传入的泛型参数必须满足要求。extends的含义是继承,例如A extends B,也就意味着A是B的子类型。

type IResponseCode<T extends number> = T extends 100 | 200 | 300 | 400 ? 'success' : 'error';

type Response1 = IResponseCode<100>; // success
type Response2 = IResponseCode<500>; // error
type Response3 = IResponseCode<'200'>; // 类型'string'不满足约束'number'

多泛型关联

我们不仅可以同时传入多个泛型参数,还可以让它们之间产生关联。

type IConditional<T, Condition, Truthy, Falsy> = T extends Condition ? Truthy : Falsy;

type IResult1 = IConditional<'Yancy', string, 'true', 'false'>; // true
type IResult2 = IConditional<'Yancy', boolean, 'true', 'false'>; // false

多个泛型参数其实很类似于一个函数中接受多个参数,它的内部逻辑将变得更加的复杂,需要进行的判断也就更多。

对象类型中的泛型

例如我们在定义接口的响应值的类型时,返回的状态码,错误信息等我们都能够统一约束其类型,但具体的返回值,不同的接口是不一致,这是我们就可以使用泛型来进行定义:

interface IResponse<T = unknown> {
    code: number;
    success: boolean;
    msg?: string;
    data: T
}

函数中的泛型

假设我们有这么一个需求,声明一个函数,我们传入什么类型的参数,我们的返回值类型也必须和它一致。

function getValue<T>(val: T): T {}

我们声明了一个泛型参数T,并将参数的类型与返回值的类型都指向了这个泛型参数,我们在函数接收到参数时,就会自动把T填充为这个参数的类型,这样我们就不用提前去进行类型定义,而且返回值类型与参数类型都是通过泛型参数进行计算的。

4. 结语

总算重新看了一遍TS的高级类型,希望在工作过程中逐步运用起来吧,而不是继续写anyscript。以前总是偷懒,自制力非常的差,每次遇到类型相关的问题都想直接绕过,在TS这一块儿基本没什么进步,最近在看林不渡大佬的小册,顺便记录一波,以便后期进行回顾。前路漫漫,加油吧!

5. 参考资料

  1. TypeScript全面进阶指南
  2. TypeScript中文网