TypeScript 类、泛型的实践 | 青训营

250 阅读8分钟

本文旨在探讨TypeScript中的类、泛型的使用方法和场景,以及如何使用类型约束来增加代码的灵活性和安全性。

类的概述

在早期的JavaScript开发中(ES5)需要通过函数和原型链来实现类和继承。 从ES6开始,引入了class关键字,可以更加方便的定义和使用类。

TypeScript是JavaScript的超集,也支持使用class关键字,还支持对类的属性和方法等进行静态类型检测。

虽然在JavaScript的开发过程中,更加习惯于函数式编程,而不是面向对象编程。

React开发中,目前更多使用的函数组件以及结合Hook的开发模式

在Vue3开发中,目前也更加推崇使用Composition API

但是在封装某些业务的时候,类也具有更强大封装性。 类的定义我们通常会使用class关键字:

  • 在面向对象的编程中,任何事物都可以使用类的结构来描述
  • 类中可以包含一些自己特有的属性和方法
  • 类也很好的诠释了面向对象的三大特性,继承、封装、多态

类的定义最基本方式

  1. 定一个Person 类
  2. 在这个类里面定义属性或者方法
  3. 在new 创建一个对象
  4. 在创建对象的时候,会执行构造器
  5. 通过this.name 拿到 Person 里面的那个name,然后把constrector 里面的name 赋值给this.name
/**
 1.定一个Person 类
 2.在这个类里面定义属性或者方法
 3.在new 创建一个对象
 4.在创建对象的时候,会执行构造器
 5.通过this.name 拿到 Person 里面的那个name,然后把constrector 里面的name 赋值给this.name
 */
class Person{
    name:string;
    age:number;
    constructor(name:string,age:number){
       this.name=name
       this.age=age
    }
    eat(){
        console.log(this.name+"eating")
    }
}

类的基本使用

const p=new Person("jiang",18)
console.log(p.name)
console.log(p.age)

应用场景

除了日常借助类的特性完成日常业务代码,还可以将类(class)也可以作为接口,尤其在 React 工程中是很常用的,如下:

export default class Carousel extends React.Component<Props, State> {}

由于组件需要传入 props 的类型 Props ,同时有需要设置默认 props 即 defaultProps,这时候更加适合使用class作为接口。

先声明一个类,这个类包含组件 props 所需的类型和初始值:

// props的类型
export default class Props {
  public children: Array<React.ReactElement<any>> | React.ReactElement<any> | never[] = []
  public speed: number = 500
  public height: number = 160
  public animation: string = 'easeInOutQuad'
  public isAuto: boolean = true
  public autoPlayInterval: number = 4500
  public afterChange: () => {}
  public beforeChange: () => {}
  public selesctedColor: string
  public showDots: boolean = true
}

当我们需要传入 props 类型的时候直接将 Props 作为接口传入,此时 Props 的作用就是接口,而当需要我们设置defaultProps初始值的时候,我们只需要:

public static defaultProps = new Props()

Props 的实例就是 defaultProps 的初始值,这就是 class 作为接口的实际应用,我们用一个 class 起到了接口和设置初始值两个作用,方便统一管理,减少了代码量。

什么是泛型

泛型 (Generics) 是指在定义函数、接口或类的时候,不预先指定具体的类型。而在使用的时候在指定类型的一种特性。

通俗理解:泛型就是解决 类、接口、方法 的复用性、以及对不特定数据类型的校验。

通俗的解释: 泛型是类型系统中的 参数 ,主要作用是为了类型的重用 。从上面定义可以看出:它只会用函数 接口和类中。

它和 js 程序中的函数参数是两个层面的事物 (虽然意义是相同的),因为 typescript 是静态类型系统 是在 js 进行编译时进行类型检查的系统。

因此,泛型这种参数,实际上是编译过程中的运行时使用。之所以称它为参数,是因为它具备和函数参数一模一样的特性。

function increse(param) {
	// ...
}

而类型中我们如此使用泛型

function increse<T>(param: T): T{
	// ...
}

当 param 为一个类型时, T 被赋值为这个类型 在返回值中 T 即为该类型从而进行类型检查。

泛型的基本使用

下面是一些常见的使用泛型的情况:

  1. 泛型函数:定义一个函数时,可以使用泛型来处理输入参数或返回值的类型。
function getData1(value:string):string{    //同时返回string类型和number类型(代码冗余)
    return value;
}
function getData2(value:number):number{    //同时返回string类型和number类型(代码冗余)
    return value;
}
 
 
function getData<T>(value:T):any{    //T 表示泛型,具体什么类型是调用这个方法的时候决定的
    return 'erha';
}
getData<number>(123);    //参数必须是 number
getData<string>('字符串'); //参数必须是 string
  1. 泛型类:类可以使用泛型来定义属性、方法或构造函数的参数。
class Minclass{    //实例中的方法参数只支持 number 类型
    public list:number[] = [];
    add(num:number){
        this.list.push(num)
    }
    min():number{
        var minNum = this.list[0];
 
        for(var i=0;i<this.list.length;i++){
            if(minNum > this.list[i]){
                minNum = this.list[i];
            }
        }
        return minNum;|
    }
}
 
class Minclass<T>{    //通过泛型,可以让实例中的方法支持多种类型的参数
    public list:T[] = [];
    add(num:T){
        this.list.push(num)
    }
    min():T{
        var minNum = this.list[0];
 
        for(var i=0;i<this.list.length;i++){
            if(minNum > this.list[i]){
                minNum = this.list[i];
            }
        }
        return minNum;|
    }
}
var ml = new Minclass<number>();    /*实例化类并且制定了类的T代表的类型是number*/
m1.add(1);
alert(ml.min());
 
var m2 = new Minclass<string>();    /*实例化类并且制定了类的T代表的类型是string*/
m1.add('a');
alert(ml.min());
  1. 泛型接口:接口也可以使用泛型来定义属性、方法或函数的类型。
interface ConfigFn{    //函数类型接口,约束类型单一
    (value1:string, value2:string):string;
}
var setData:ConfigFn = function(value1:string, value2:string):string{
    return value1 + value2;
}
setData('name', 'erha');
 
 
interface ConfigFn{    //泛型接口,可以允许多种类型的参数
    <T>(value:T):T;
}
var getData:ConfigFn = function<T>(value:T):T{
    return value;
}
getData<string>('erha');
 
 
interface ConfigFn<T>{    //也可以这样写
    (value:T):T;
}
var getData:ConfigFn = function<T>(value:T):T{
    return value;
}
getData<string>('erha');

需要注意的是,TypeScript 的泛型只在编译时起作用,运行时会被擦除,因为 JavaScript 是一种动态类型的语言。因此,泛型在编译阶段会进行类型检查,但在真正执行时,并不会对泛型进行运行时类型检查。

应用场景

通过上面初步的了解,后述在编写 typescript 的时候,定义函数,接口或者类的时候,不预先定义好具体的类型,而在使用的时候在指定类型的一种特性的时候,这种情况下就可以使用泛型。

泛型约束

默认情况下,泛型函数的类型变量 Type 可以代表任意类型,这导致,无法访问任何属性

function id<Type>(value: Type): Type {
  // 报错: 类型“Type”上不存在属性“length”
  console.log(value.length)
  return value
}

id<string>('a')

解释:Type 可以代表任意类型,无法保证一定存在 length 属性,比如 number 类型就没有 length

此时,就需要为泛型添加约束来收缩类型(缩窄类型取值范围)

添加约束

比如,想要访问参数 value 的 length 属性,就可以添加以下约束:

// 创建一个接口
// interface ILength { length: number }
type Length = { length: number }

// Type extends Length 添加泛型约束
// 解释:表示传入的 类型 必须满足 Length 类型的要求才行,也就是得有一个 number 类型的 length 属性
function id<Type extends Length>(value: Type): Type {
  console.log(value.length)
  return value
}
  • 创建描述约束的接口 ILength,该接口要求提供 length 属性
  • 通过 extends 关键字使用该接口,为泛型(类型变量)添加约束
  • 该约束表示:传入的类型必须具有 length 属性

注意:传入的实参(比如,数组)只要有 length 属性即可(类型兼容性)

多个类型变量

泛型的类型变量可以有多个,并且类型变量之间还可以约束(比如,第二个类型变量受第一个类型变量约束) 比如,创建一个函数来获取对象中属性的值:

function getProp<Type, Key extends keyof Type>(obj: Type, key: Key) {
  return obj[key]
}
let person = { name: 'jack', age: 18 }
getProp(person, 'name')
  • 添加了第二个类型变量 Key,两个类型变量之间使用 , 逗号分隔
  • keyof 关键字接收一个对象类型,生成其键名称(可能是字符串或数字)的联合类型
  • 本示例中 keyof Type 实际上获取的是 person 对象所有键的联合类型,也就是:‘name’ | ‘age’
  • 类型变量 Key 受 Type 约束,可以理解为:Key 只能是 Type 所有键中的任意一个,或者说只能访问对象中存在的属性

当然,第一个参数也可以添加类型约束,比如:

// Type extends object 表示: Type 应该是一个对象类型,如果不是 对象 类型,就会报错
function getProperty<Type extends object, Key extends keyof Type>(obj: Type, key: Key) {
  return obj[key]
}

总结

TypeScript类和泛型是开发中常用的特性,并且能够显著提升代码的可维护性和复用性。通过使用类,可以封装数据和行为,实现面向对象编程。通过使用泛型,可以延迟指定具体类型,实现代码的灵活性和复用性。