TypeScript 类、泛型的实践
本文旨在探讨 TypeScript 中的类、泛型的使用方法和场景,以及如何使用类型约束来增加代码的灵活性和安全性。
类的概述
在早期的 JavaScript 开发中(ES5)需要通过函数和原型链来实现类和继承。从 ES6 开始,引入了 class 关键字,可以更加方便的定义和使用类。
TypeScript 是 JavaScript 的超集,也支持使用 class 关键字,还支持对类的属性和方法等进行静态类型检测。
虽然在 JavaScript 的开发过程中,更加习惯于函数式编程,而不是面向对象编程。在 React 开发中,目前更多使用的是函数组件以及结合 Hook 的开发模式,在 Vue3 开发中,目前也更加推崇使用 Composition API。但是在封装某些业务的时候,类也具有更强大封装性。
类的定义最基本方式
定义一个 Person 类,在这个类里面定义属性或者方法,在 new 创建一个对象,在创建对象的时候,会执行构造器,通过 this.name 拿到 Person 里面的那个 name,然后把 constructor 里面的 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 工程中是很常用的。比如,组件需要传入 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)是指在定义函数、接口或类的时候,不预先指定具体的类型。而在使用的时候在指定类型的一种特性。
通俗理解:泛型就是解决类、接口、方法的复用性、以及对不特定数据类型的校验。
通俗的解释:泛型是类型系统中的参数.
在类的继承中,子类可以继承父类的属性和方法,并且可以进行重写或者扩展。例如:
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
class Snake extends Animal {
constructor(name: string) {
super(name);
}
move(distanceInMeters = 5) {
console.log("Slithering...");
super.move(distanceInMeters);
}
}
class Horse extends Animal {
constructor(name: string) {
super(name);
}
move(distanceInMeters = 45) {
console.log("Galloping...");
super.move(distanceInMeters);
}
}
let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino");
sam.move();
tom.move(34);
在上面的例子中,Snake类和Horse类都继承了Animal类,同时重写了父类的move方法。在Snake类中,重写了move方法并且调用了父类的move方法,而在Horse类中,重写了move方法但是没有调用父类的move方法,因此只输出了Galloping...。
另外,在TypeScript中,类可以实现接口,这种方式可以让我们在类中定义一些必须实现的方法,从而提高代码的可读性和可维护性。例如:
interface ClockInterface {
currentTime: Date;
setTime(d: Date): void;
}
class Clock implements ClockInterface {
currentTime: Date = new Date();
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) {}
}
在上面的例子中,Clock类实现了ClockInterface接口,因此必须实现接口中的currentTime和setTime方法。
关于泛型的使用,我们可以进一步了解一下泛型约束。泛型约束可以让我们限制泛型的类型范围,从而提高代码的类型安全性和可读性。例如:
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
loggingIdentity("hello");
在上面的例子中,泛型函数loggingIdentity的类型参数T必须满足Lengthwise接口的约束,即必须包含length属性。这样,在函数内部就可以访问arg.length属性,从而保证了类型安全性。
除了泛型函数之外,我们还可以使用泛型类和泛型接口,并且同样可以进行泛型约束。例如:
interface Compare<T> {
(a: T, b: T): number;
}
class SortedArray<T> {
constructor(public data: T[], public compare: Compare<T>) {}
sort() {
this.data.sort(this.compare);
}
}
let sortedArray = new SortedArray([3, 1, 4, 1, 5, 9, 2, 6, 5], (a, b) => a - b);
sortedArray.sort();
console.log(sortedArray.data);
在上面的例子中,SortedArray类使用了泛型约束,并且使用了Compare泛型接口来限制比较函数的类型。这样,在sort方法中就可以使用this.compare来进行比较操作,从而保证了类型安全性。
总的来说,类和泛型是TypeScript中非常重要的特性,可以帮助我们提高代码的可维护性和复用性。在实际开发中,我们可以根据具体的需求灵活运用这些特性,从而写出更加优雅和安全的代码。
一、类的高级用法
- 继承
在 TypeScript 中,类可以通过继承来扩展已有的类。子类可以继承父类的属性和方法,并且可以添加自己的属性和方法。下面是一个简单的继承示例:
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
class Dog extends Animal {
bark() {
console.log('Woof! Woof!');
}
}
const dog = new Dog('Buddy');
dog.bark(); // 输出 "Woof! Woof!"
dog.move(10); // 输出 "Buddy moved 10m."
在上面的示例中,Dog 类继承了 Animal 类,并且添加了自己的 bark 方法。通过 new Dog('Buddy') 创建的实例,既可以调用 Animal 类的 move 方法,也可以调用 Dog 类的 bark 方法。
- 抽象类
抽象类是不能被实例化的类,它只能被用作其他类的基类。抽象类可以定义抽象方法,抽象方法是没有具体实现的方法,需要在子类中实现。下面是一个抽象类的示例:
abstract class Animal {
abstract makeSound(): void;
move(distanceInMeters: number = 0) {
console.log(`Animal moved ${distanceInMeters}m.`);
}
}
class Dog extends Animal {
makeSound() {
console.log('Woof! Woof!');
}
}
const dog = new Dog();
dog.makeSound(); // 输出 "Woof! Woof!"
dog.move(10); // 输出 "Animal moved 10m."
在上面的示例中,Animal 类是一个抽象类,它定义了一个抽象方法 makeSound 和一个具体方法 move。Dog 类继承了 Animal 类,并且实现了 makeSound 方法。通过 new Dog() 创建的实例,既可以调用 Animal 类的 move 方法,也可以调用 Dog 类的 makeSound 方法。
二、泛型的高级用法
- 高级类型约束
在 TypeScript 中,可以使用类型约束来限制泛型类型的范围。例如,可以使用 extends 关键字来限制泛型类型必须是某个类的子类,或者必须实现某个接口。下面是一个使用类型约束的示例:
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
loggingIdentity('hello'); // 输出 5
loggingIdentity([1, 2, 3]); // 输出 3
loggingIdentity({ length: 10, value: 3 }); // 输出 10
在上面的示例中,loggingIdentity 函数使用了类型约束 T extends Lengthwise,表示泛型类型 T 必须实现 Lengthwise 接口,即必须包含 length 属性。因此,loggingIdentity 函数可以接受字符串、数组和对象等类型作为参数,并返回相同的类型。
- 泛型类型别名
泛型类型别名是一种给泛型类型起别名的方式,可以方便地在多个地方使用同一个泛型类型。下面是一个泛型类型别名的示例:
type NameValuePair<T> = {
name: string;
value: T;
};
function getValue<T>(pair: NameValuePair<T>): T {
return pair.value;
}
const pair1: NameValuePair<number> = { name: 'age', value: 18 };
const pair2: NameValuePair<string> = { name: 'name', value: 'Tom
- 高级类型约束(Advanced Type Constraints) 在 TypeScript 中,我们可以使用高级类型约束来对泛型进行更精确的类型控制。例如,我们可以使用条件类型(Conditional Types)来实现一些复杂的类型映射。
type IsArray<T> = T extends any[] ? true : false;
type result = IsArray<number[]>; // true
type result2 = IsArray<number>; // false
在上面的示例中,我们定义了一个条件类型 IsArray,它接受一个类型参数 T。如果 T 是一个数组类型,则 IsArray 返回 true,否则返回 false。
- 泛型类型别名(Generic Type Aliases) 泛型类型别名允许我们为复杂的泛型类型定义别名,以提高代码的可读性和可维护性。
type Result<T> = {
success: boolean;
data: T;
};
type NumberResult = Result<number>;
type StringResult = Result<string>;
在上面的示例中,我们使用泛型类型别名 Result 定义了一个结果类型,它包含一个布尔值字段 success 和一个泛型字段 data,data 可以是任意类型。然后我们使用 Result 和 Result 分别创建了 NumberResult 和 StringResult 类型别名。
- 类型推断和提取(Type Inference and Extracting) TypeScript 会根据上下文对变量的类型进行推断,从而可以在代码中省略类型注解。此外,我们还可以使用 typeof 和 keyof 来提取类型的属性或方法。
const user = { name: 'John', age: 30 };
type User = typeof user; // { name: string, age: number }
type UserKeys = keyof User; // 'name' | 'age'
在上面的示例中,我们定义了一个名为 user 的对象。然后使用 typeof user 提取了 user 的类型,并将其赋值给 User 类型别名。接着,使用 keyof User 提取了 User 类型的属性名称,得到了 UserKeys 类型。
这些是 TypeScript 中一些高级的特性和用法,它们可以帮助我们更好地利用 TypeScript 的类型系统来编写健壮、可维护的代码。。