接口和类
接口(Interface)和类(Class)是TypeScript中的两个核心概念,接口用于定义对象的结构约束,类则用于实现面向对象编程。本篇笔记将详细介绍TypeScript中接口和类的定义与使用方法。
接口
接口(Interface)是TypeScript中用于定义对象结构的一种方式,它描述了对象应该具有哪些属性和方法,但不包含具体实现。接口在编译后会被完全移除,不会产生任何JavaScript代码,它仅用于编译阶段的类型检查。
定义接口
使用interface关键字可以定义一个接口。
interface User {
name: string;
age: number;
greet(): string;
}
const user: User = {
name: "Tom",
age: 18,
greet() {
return `Hello, my name is ${this.name} and I am ${this.age} years old.`;
}
};
上面代码中,我们定义了User接口,它约束了对象必须包含name和age两个属性以及greet()方法。当我们创建user对象时,TypeScript会检查对象结构是否符合接口定义,属性缺失或类型不匹配都会报错。
可选属性和只读属性
接口中可以使用?标记可选属性,使用readonly标记只读属性。
interface User {
readonly id: number;
name: string;
age?: number;
}
const user: User = {
id: 1,
name: "Tom",
};
// user.id = 2; // 错误,只读属性不可修改
代码中,id是只读属性,初始化后不可修改;age是可选属性,创建对象时可以省略。
函数类型接口
接口不仅可以描述对象结构,还可以描述函数类型。
interface SearchFunc {
(source: string, keyword: string): boolean;
}
const search: SearchFunc = (source, keyword) => {
return source.includes(keyword);
};
不过实际上单纯用接口描述一个函数比较少见,一般描述函数建议使用type声明函数类型表达式,而非用interface来实现,有关函数类型表达式具体可以参考下一章节。
索引签名
当我们不确定对象会有哪些属性,但知道属性的类型规律时,可以使用索引签名。
interface StringMap {
[key: string]: string;
}
const map: StringMap = {
name: "Tom",
city: "Beijing",
};
索引签名支持string和number两种类型作为键。
接口继承
接口可以通过extends关键字继承其他接口,且接口支持多继承。
interface Animal {
name: string;
}
interface Runnable {
speed: number;
}
interface Dog extends Animal, Runnable {
breed: string;
}
const dog: Dog = {
name: "Buddy",
speed: 20,
breed: "Labrador",
};
类
TypeScript中的类是对ES6类语法的增强,增加了访问修饰符、抽象类等特性。
定义类
使用class关键字定义类。
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
greet(): void {
console.log(`Hello, I'm ${this.name}`);
}
}
const person = new Person("Tom", 18);
person.greet();
代码中,我们定义了Person类,包含两个属性和一个方法。constructor是构造函数,在创建实例时自动调用。
访问修饰符
TypeScript提供了三种访问修饰符来控制成员的可访问性:
public:公开的,任何地方都可以访问(默认)private:私有的,只能在类内部访问protected:受保护的,只能在类内部和子类中访问
class Person {
public name: string;
private age: number;
protected id: number;
constructor(name: string, age: number, id: number) {
this.name = name;
this.age = age;
this.id = id;
}
private getAge(): number {
return this.age;
}
}
const person = new Person("Tom", 18, 1);
console.log(person.name); // 正确
// console.log(person.age); // 错误,私有属性不可访问
参数属性简写
TypeScript提供了一种简写方式,可以在构造函数参数上直接添加访问修饰符,自动创建并初始化类属性,下面两种写法等价。
class Person {
constructor(
public name: string,
private age: number
) {}
}
class Person {
public name: string;
private age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
只读属性
使用readonly关键字可以将属性标记为只读,只读属性只能在声明时或构造函数中初始化。
class Person {
readonly id: number;
constructor(id: number) {
this.id = id;
}
}
const person = new Person(1);
// person.id = 2; // 错误,只读属性不可修改
静态成员
使用static关键字可以定义静态属性和静态方法,静态成员属于类本身而非实例,因此可以通过类名直接调用。
class Counter {
static count: number = 0;
static increment(): void {
Counter.count++;
}
}
Counter.increment();
console.log(Counter.count); // 1
类的继承
使用extends关键字可以实现类的继承。
class Animal {
constructor(public name: string) {}
move(): void {
console.log(`${this.name} is moving`);
}
}
class Dog extends Animal {
constructor(name: string, public breed: string) {
super(name);
}
bark(): void {
console.log("Woof!");
}
move(): void {
console.log(`${this.name} is running`);
}
}
const dog = new Dog("Buddy", "Labrador");
dog.move(); // Buddy is running
dog.bark(); // Woof!
代码中,Dog类继承了Animal类,通过super()调用父类构造函数,并重写了move()方法。
抽象类
使用abstract关键字可以定义抽象类和抽象方法。抽象类不能被实例化,只能作为基类被继承;抽象方法没有具体实现,必须在子类中实现。
abstract class Shape {
abstract getArea(): number;
printArea(): void {
console.log(`Area: ${this.getArea()}`);
}
}
class Rectangle extends Shape {
constructor(
private width: number,
private height: number
) {
super();
}
getArea(): number {
return this.width * this.height;
}
}
const rect = new Rectangle(10, 20);
rect.printArea(); // Area: 200
类实现接口
类可以使用implements关键字实现一个或多个接口,这要求类必须实现接口中定义的所有属性和方法。
interface Printable {
print(): void;
}
interface Loggable {
log(): void;
}
class Document implements Printable, Loggable {
print(): void {
console.log("Printing...");
}
log(): void {
console.log("Logging...");
}
}
接口实现是TypeScript中实现多态的重要方式,它允许我们定义统一的契约,不同的类可以有不同的实现。
接口和类型别名的区别
在TypeScript中,interface和type都可以用于定义对象类型,但两者存在一些区别。
// 接口定义
interface User {
name: string;
}
// 类型别名定义
type UserType = {
name: string;
};
主要区别如下:
- 接口可以重复声明并自动合并,类型别名不可以
- 接口只能描述对象结构,类型别名可以描述任意类型
- 类只能实现接口,不能实现类型别名
// 接口声明合并
interface User {
name: string;
}
interface User {
age: number;
}
// 合并后等价于
interface User {
name: string;
age: number;
}
一般来说,描述对象结构优先使用接口,描述联合类型、交叉类型等复杂类型时可使用类型别名。