TypeScript最佳实践(六)泛型

140 阅读9分钟

1. 泛型简介

1. 什么是泛型

泛型(Generics)是允许同一个函数接受不同类型参数的一种模板。相比于使用 any 类型,使用泛型来 创建可复用的组件要更好,因为泛型会保留参数类型。

设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:类的实例成员、类的方法、函 数参数和函数返回值。

2. 使用泛型好处

好处1: 编译期对类上调用方法或属性时的泛型类型进行安全检查(类型安全检查),不符合泛型实际参数类型(泛型实参类型) 就编译通不过,防止不符合条件的数据增加进来。

好处2: 一种泛型类型被具体化成某种数据类型后,该数据类型的变量获取属性和方法时会有自动提示,这无疑提高代码开发效率和减少出错率。

2. 常见的泛型用法

1. 泛型语法

function identity<T>(arg: T): T { 
    return arg; 
}

2. 泛型接口

interface GenericIdentityFn<T> { 
    (arg: T): T; 
}

3. 泛型类

class GenericNumber<T> { 
    zeroValue: T; 
    add: (x: T, y: T) => T; 
}

4. 泛型约束

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  // Now we know it has a .length property, so no more error
    return arg;
}

3. 泛型工具

1. typeof

typeof 操作符可以用来获取一个变量声明或对象的类型

interface Person {
    name: string;
    age: number;
}
const sem: Person = { name: 'semlinker', age: 33 };

type Sem = typeof sem; // -> Person


function toArray(x: number): Array<number> {
    return [x];
}
type Func = typeof toArray; // -> (x: number) => number[]

2. keyof

keyof 操作符是在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。(所有public属性+所有的public方法名组成的联合类型)

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

type K1 = keyof Person; // "name" | "age"
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join"
type K3 = keyof { [x: string]: Person }; // string | number

在 TypeScript 中支持两种索引签名,数字索引和字符串索引:

interface StringArray {
    // 字符串索引 -> keyof StringArray => string | number 
    [index: string]: string;
}

interface StringArray1 {
    // 数字索引 -> keyof StringArray1 => number 
    [index: number]: string;
}

为了同时支持两种索引类型,就得要求数字索引的返回值必须是字符串索引返回值的子类。其中的原因 就是当使用数值索引时,JavaScript 在执行索引操作时,会先把数值索引先转换为字符串索引。所以 keyof { [x: string]: Person } 的结果会返回 string | number

3.in

in 用来遍历枚举类型:

type Keys = "a" | "b" | "c"

type Obj = {
    [p in Keys]: any
} // -> { a: any, b: any, c: any }

4. extends

添加泛型约束

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T { 
    console.log(arg.length);
    return arg;
}

分配式extends

T extends U ? X : Y

type Diff<T, U> = T extends U ? never : T; // 找出T的差集
type Filter<T, U> = T extends U ? T : never; // 找出交集
type T30 = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // => "b" | "d"
// <"a" | "b" | "c" | "d", "a" | "c" | "f">
// 相当于
// <'a', "a" | "c" | "f"> | <'b', "a" | "c" | "f"> | <'c', "a" | "c" | "f"> | <'d', "a" | "c" | "f">
type T31 = Filter<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // => "a" | "c"

5. infer

在extends语句中,还支持infer关键字,可以推断一个类型变量,高效的对类型进行模式匹配。但是,这个类型变量只能在true的分支中使用

type ReturnType<T> = T extends ( ...args: any[]) => infer R ? R : any;
``·

以上代码中 infer R 就是声明一个变量来承载传入函数签名的返回值类型,简单说就是用它取到函数返回值的类型方便之后使用。

```typescript
// 解读: 假如泛型变量T是 () => R 的`子集`,那么返回通过infer获取到的函数返回值,否则返回boolean类型
type Func<T> = T extends () => infer R ? R : boolean;
let func1: Func<number>; // => boolean
let func2: Func<''>; // => boolean
let func3: Func<() => Promise<number>>; // => Promise<number>

// 同上,但当a、b为不同类型的时候,返回不同类型的联合类型
type Obj<T> = T extends {a: infer VType, b: infer VType} ? VType : number;
let obj1: Obj<string>; // => number
let obj2: Obj<true>; // => number
let obj3: Obj<{a: number, b: number}>; // => number
let obj4: Obj<{a: number, b: () => void}>; // => number | () => void

4. 泛型实现ArrayList

class ArrayList<T = any> {
    //第一步:定义一个引用属性【数组】
    public element: Array<T>
    constructor() {
        this.element = [];
    }
    public index: number = 0;
    // 往数组中添加元素
    public add(ele: T) {
        this.checkIndex();
        this.element[this.index++] = ele;
    }
    public checkIndex() {
        if (this.index < 0) {
            throw new Error("数组下标不能为零");
        }
    }
    // 第二步:根据索引来查询数组中指定元素
    get(index: number): T {
        return this.element[index];
    }

    // 第三步: 显示方法
    show() {
        this.element.forEach((ele) => {
          console.log(ele);
        })
    }

    remove(value: number): number
    remove(value: object): object
    //remove(value: number | object): number | object {
    remove(value: any): any {
        this.element = this.element.filter((ele, index) => {
            //如果是根据数字【元素索引】去删除元素,remove方法返回的是一个数字
            if (typeof value === "number") {
                return value !== index
            } else {
                // 如果是根据对象去删除元素,remove方法返回的是一个对象
                return value !== ele
            }
        })
        return value;
    }
}

1. object 为什么不能替代类上的泛型?

原因一:编译期间 object 无法进行类型安全检查,而泛型在编译期间可以进行类型安全检查

object 接受也只能接受所有的 object 类型的变量,比如有 Customer、Student、Dog 类的实例都是对象类型,或者自己定义的对象,都可以传递给 object 类型的方法参数或属性, 但如果我们只希望添加Customer类的对象,当添加其他类的对象必须出现编译错误,但是 object 无法做到,就只能用泛型了。

原因二: object 类型数据无法接受非 object 类型的变量,只能接受 object 类型的变量,泛型能轻松做到

正因为 object 接受也只能接受所有的 object 类型的变量,那么如果有一个集合类[数组封装类]有一个 add 方法,允许每次添加指定类型的变量到 add 方法的参数,比如:我们第一轮的希望添加 10 次字符串类型的变量,第二轮的希望添加 10 次整数类型变量,第三轮的希望添加 10 次顾客类型的变量,泛型轻松做到。object 类型数据无法接受任意非 object 类型的变量,object 只能接受所有的 object 类型的变量。

原因三: object 类型数据获取属性和方法时无自动提示,泛型有自动提示

一种泛型类型被具体化成某种数据类型后,该数据类型的变量获取属性和方法时会有自动提示,提高代码开发效率和减少出错率,但在 object 类型的变量无法获取数据类型的属性和方法,降低了体验感和开发效率。

2. 【 TS 泛型类】详细讲解+透彻总结 any 为什么不能替代类上的泛型?

原因一:编译期间 any 无法进行类型安全检查,而泛型在编译期间可以进行类型安全检查 我们学过: any 是所有类型的父类,也是所有类型的子类如果我们现在是一个宠物店类,希望只能添加 Dog 类,当调用 add 方法添加 Customer、Student 类必定出现编译错误,从而保证了类型安全检查,但是 any 类型无法保证类型安全检查,可以为任意类型,包括 string,number,boolean,null,undefined,never,void,unknown 基础数据类型和数组,类,接口类型, type 类型的变量全部能接受,不会进行无法进行类型安全检查。

原因二:any 类型可以获取任意数据类型的任何属性和任意方法而不会出现编译错误导致潜在错误风险,而泛型却有效的避免了此类问题发生 any 类型可以获取任何属性和任意方法而不会出现编译错误,因为any可以代表任意数据类型来获取任意属性和任意方法,但是泛型类型被具体化成某种数据类型后,该数据类型的变量调用该数据类型之外的属性和方法时,出现编译错误,这也减少了代码隐藏潜在错误的风险。

原因三: any 类型数据获取属性和方法时无自动提示,泛型有自动提示

彩蛋:any 类型可以代表任意数据类型来获取任何属性和任意方法而不会出现编译错误,因为any可以代表任意数据类型来获取任意属性和任意方法:【 any 的这个特性是一把双刃剑,当我们需要这么使用,它给我们带来方便,但是大多数情况下我们是不需要这么做的】。

5. T extends object + extends keyof

class ObjectImpl<T extends object, K extends keyof T>{
    object!: T;
    key!: K
    constructor(object_: T, key_: K) {
        this.object = object_;
        this.key = key_;
    }
    getValue() {
        return this.object[this.key]
    }
    setValue(newVal: T[K]) {
        this.object[this.key] = newVal;
    }
}

6. 泛型函数重载

// 英文、数字数组排序  
function quickSort<T>(arr: Array<T>): T[] {
    if (arr.length < 2) { return arr }
    var left: Array<T> = [];
    var right: Array<T> = [];
    var mid = arr.splice(Math.floor(arr.length / 2), 1)[0];
    for (var i = 0; i < arr.length; i++) {
        if (arr[i] < mid) {
            left.push(arr[i]);
        } else {
            right.push(arr[i])
        }
    }
    return quickSort(left).concat(mid, quickSort(right))
}

// 字符串自排序
function strSelfSort(str: string, count: number = 5): string {
    // (1) 字符串拆分成数组
    let strArray = str.split('');
    // (2) 数组进行使用快速排序算法来排序
    let strSortArray = quickSort(strArray);
    // (3) 重新把排好序的数组连接成一个字符串返回
    let strResult = strSortArray.join('');
    return strResult.length > 10 ? strResult.substr(0, count) + "..." : strResult;
}

//   中文排序
function sortChinese<T>(arr: Array<T>): T[] {
    return arr.sort(function (firstnum, secondnum) {
        return (firstnum as any).localeCompare(secondnum, "zh-CN")
    })
}

// 判断数组中是否有中文元素
function isChinese<T>(arr: Array<T>): boolean {
    var pattern1 = /[\u4e00-\u9fa5]+/g;
    return arr.some((item) => {
        return pattern1.test(item as any)
    })
}

function sort(data: string, count?: number): string // 字符串排序
function sort<T>(data: T, count?: number): T//分工明确
function sort(data: any, count: number = 5): any {
    if (typeof data === "string") {//如果是字符串
        return strSelfSort(data, count)// 按照字符串自排序
    }
    if (data instanceof Array) {//如果data是数组
        if (isChinese(data)) {//如果是中文数组
            return sortChinese(data);
        }
        let newArr = data.map((item) => {
            return typeof item === "string" ? strSelfSort(item) : item
        })
        //英文、数字数组排序
        return quickSort(newArr as any);
    }
}

var str = "bdfaerafdfsd"
let strResult = sort(str, 6)
console.log("长度为:", strResult.length, "字符串", strResult)

var numArr = [3, 1.883332, 8, 9, 20, 15, 2, 7, 13, 11, 19, 18, 5, 6, 17, 4];
console.log(sort(numArr));
//sort(numArr)
let result = sort<number[]>(numArr)
result.forEach((item) => {
  console.log(item.toFixed(2));
})

let strArr: Array<string> = ["cba", "kkdf", "ndf", "bcdf", "dfd", "cdf"]
console.log(sort(strArr));

var chineseArr = ["武汉", "郑州", "太原", "济南", "沈阳", "大连"];
console.log(sort(chineseArr));

7. 泛型函数

1. 通用函数类型

// 通用函数类型
type commonFunc = (...args: any) => any

interface commonFuncInter {
    eat: (...args: any) => any
}

interface commonFuncInter {
  (...args: any): any
}

let func: commonFuncInter = function (count: string, money: number): void {

}

2. 构造函数类型

new 不是创建的意思,而是表示后面的类型是一个类构造函数对象变量的类型【类构造函数的类型】

type constructor = new (...args: any) => any

泛型工厂函数

class  CommercialBank {
    public address: string = "beijing"
    public name: string = "wangwu"
    static count: number

    constructor(name: string, address: string) {
        this.address = address;
        this.name = name
    }
    
    loan(): void {
        console.log(this.name + " 银行贷款");
    }
}

let o:CommercialBank=new CommercialBank("Df","Df")


//function createInstanceFactory(Constructor:new (...arg: any) => any) {
function createInstanceFactory(Constructor: { new(...arg: any): any }) {
    console.log(Constructor.name + "被创建对象");
    return new Constructor("广大银行", "万绿园");
}
// 使用工厂函数来创建我们的CommercialBank
let con = createInstanceFactory(CommercialBank)

工厂函数类型的简单使用

type ConstructorType = new (...arg: any) => any
let Constructor: ConstructorType = CommercialBank 
let con2 = new Constructor("广大银行", "万绿园");
//let con2 = new CommercialBank("广大银行", "万绿园");

泛型工厂函数

function createInstanceFactory2<T>(Constructor: { new(...arg: any): T }):T {
  console.log(Constructor.name + "被创建对象");
  return new Constructor("广大银行", "万绿园");
}
// type 或者interface 
// 
let con3 = createInstanceFactory2<CommercialBank>(CommercialBank)
con3.loan();

8. 装饰器替代写法

type MyClassDecorator = <T>(targetClass: { new(...args: any[]): T }) => any
function Controller(rootPath: string) {
    return function <T>(targetClass: { new(...args: any[]): T }) {
    
    }
}

function Controller2(rootPath: string): MyClassDecorator {
    return function (targetClass) {
    
    }
}