2. 只读修饰符
在类中可以使用readonly
关键字将属性设置为只读:
class UserInfo { readonly name: string; constructor(name: string) { this.name = name; } } const user = new UserInfo("TypeScript"); user.name = "haha"; // error Cannot assign to 'name' because it is a read-only property 复制代码
设置为只读的属性,实例只能读取这个属性值,但不能修改。
需要注意,如果只读修饰符和可见性修饰符同时出现,需要将只读修饰符写在可见修饰符后面。
三、类的类型
1. 属性类型
(1)参数属性
在上面的例子中,都是在类的定义的顶部初始化实例属性,在 constructor 里接收参数然后对实例属性进行赋值,可以使用参数属性来简化这个过程。参数属性就是在 constructor 构造函数的参数前面加上访问限定符:
class A { constructor(name: string) {} } const a = new A("aaa"); console.log(a.name); // error 类型“A”上不存在属性“name” class B { constructor(public name: string) {} } const b = new B("bbb"); console.log(b.name); // "bbb" 复制代码
可以看到,在定义类 B 时,构造函数有一个参数 name,这个 name 使用访问修饰符 public 修饰,此时即为 name 声明了参数属性,也就无需再显式地在类中初始化这个属性了。
(2)静态属性
在 TypeScript 中和 ES6 中一样使用static
关键字来指定属性或方法是静态的,实例将不会添加这个静态属性,也不会继承这个静态方法。可以使用修饰符和 static 关键字来指定一个属性或方法:
class Parent { public static age: number = 18; public static getAge() { return Parent.age; } constructor() { // } } const p = new Parent(); console.log(p.age); // error Property 'age' is a static member of type 'Parent' console.log(Parent.age); // 18 复制代码
如果使用了 private 修饰道理和之前的一样:
class Parent { public static getAge() { return Parent.age; } private static age: number = 18; constructor() { // } } const p = new Parent(); console.log(p.age); // error Property 'age' is a static member of type 'Parent' console.log(Parent.age); // error 属性“age”为私有属性,只能在类“Parent”中访问。 复制代码
(3)可选类属性
TypeScript 还支持可选类属性,也是使用?
符号来标记:
class Info { name: string; age?: number; constructor(name: string, age?: number, public sex?: string) { this.name = name; this.age = age; } } const info1 = new Info("TypeScript"); const info2 = new Info("TypeScript", 18); const info3 = new Info("TypeScript", 18, "man"); 复制代码
2. 类的类型
类的类型和函数类似,即在声明类时,同时声明了一个特殊的类型,这个类型的名字就是类名,表示类实例的类型;在定义类的时候,声明的除构造函数外所有属性、方法的类型就是这个特殊类型的成员。
定义一个类,并创建实例后,这个实例的类型就是创建他的类:
class People { constructor(public name: string) {} } let people: People = new People("TypeScript"); 复制代码
创建实例时指定 p 的类型为 People 并不是必须的,TS 会推断出他的类型。虽然指定了类型,但是当再定义一个和 People 类同样实现的类 Animal,并且创建实例赋值给 p 的时候,是没有问题的:
class Animal { constructor(public name: string) {} } let people = new Animal("JavaScript"); 复制代码
所以,如果想实现对创建实例的类的判断,还是需要用到instanceof
关键字。
四、类的使用
1. 抽象类
抽象类一般用来被其他类继承,而不直接用它创建实例。抽象类和类内部定义抽象方法,使用abstract
关键字:
abstract class People { constructor(public name: string) {} abstract printName(): void; } class Man extends People { constructor(name: string) { super(name); this.name = name; } printName() { console.log(this.name); } } const m = new Man(); // error Expected 1 arguments, but got 0. const man = new Man("TypeScript"); man.printName(); // 'TypeScript' const p = new People("TypeScript"); // error Cannot create an instance of an abstract class. 复制代码
这里定义了一个抽象类 People,在抽象类里定义 constructor 方法必须传入一个字符串类型参数,并把这个 name 参数值绑定在创建的实例上;使用abstract
关键字定义一个抽象方法 printName,这个定义可以指定参数,指定参数类型,指定返回类型。当直接使用抽象类 People 实例化的时候,就会报错,只能创建一个继承抽象类的子类,使用子类来实例化。
再看下面的例子:
abstract class People { constructor(public name: string) {} abstract printName(): void; } class Man extends People { // error Non-abstract class 'Man' does not implement inherited abstract member 'printName' from class 'People' constructor(name: string) { super(name); this.name = name; } } const m = new Man("TypeScript"); m.printName(); // error m.printName is not a function 复制代码
可以看到,第5行报错:非抽象类“Man”不会实现继承自“People”类的抽象成员"printName"。在抽象类里定义的抽象方法,在子类中是不会继承的,所以在子类中必须实现该方法的定义。
TypeScript 的abstract
关键字不仅可以标记类和类里面的方法,还可以标记类中定义的属性和存取器:
abstract class People { abstract name: string; abstract get insideName(): string; abstract set insideName(value: string); } class Pp extends People { name: string; insideName: string; } 复制代码
注意: 抽象方法和抽象存取器都不能包含实际的代码块。
2. 存取器
存取器就是 ES6 标准中的存值函数和取值函数,也就是在设置属性值的时候调用的函数,和在访问属性值的时候调用的函数,用法和写法和 ES6 的没有区别,可以通过getter、setter截取对类成员的读写访问:
class UserInfo { private name: string; constructor() {} get userName() { return this.name; } set userName(value) { console.log(`setter: ${value}`); this.name = value; } } const user = new UserInfo(); user.name = "TypeScript"; // "setter: TypeScript" console.log(user.name); // "TypeScript" 复制代码
五、类的接口
1. 类类型接口
使用接口可以强制一个类的定义必须包含某些内容:
interface FoodInterface { type: string; } class FoodClass implements FoodInterface { // error Property 'type' is missing in type 'FoodClass' but required in type 'FoodInterface' static type: string; constructor() {} } 复制代码
上面接口 FoodInterface 要求使用该接口的值必须有一个 type 属性,定义的类 FoodClass 要使用接口,需要使用关键字implements
。implements关键字用来指定一个类要继承的接口,如果是接口和接口、类和类直接的继承,使用extends,如果是类继承接口,则用implements。
注意,接口检测的是使用该接口定义的类创建的实例,所以上面例子中虽然定义了静态属性 type,但静态属性不会添加到实例上,所以还是报错,可以这样改:
interface FoodInterface { type: string; } class FoodClass implements FoodInterface { constructor(public type: string) {} } 复制代码
当然也可以使用抽象类实现:
abstract class FoodAbstractClass { abstract type: string; } class Food extends FoodAbstractClass { constructor(public type: string) { super(); } } 复制代码
2. 接口继承类
接口可以继承一个类,当接口继承了该类后,会继承类的成员,但是不包括其实现,也就是只继承成员以及成员类型。接口还会继承类的private
和protected
修饰的成员,当接口继承的这个类中包含这两个修饰符修饰的成员时,这个接口只可被这个类或他的子类实现:
class A { protected name: string; } interface I extends A {} class B implements I {} // error Property 'name' is missing in type 'B' but required in type 'I' class C implements I { // error 属性“name”受保护,但类型“C”并不是从“A”派生的类 name: string; } class D extends A implements I { getName() { return this.name; } } 复制代码
六、其他
1. 在泛型中使用类类型
先来看个例子:
const create = <T>(c: { new (): T }): T => { return new c(); }; class Info { age: number; } create(Info).age; create(Info).name; // error 类型“Info”上不存在属性“name” 复制代码
这里创建了一个 create 函数,传入的参数是一个类,返回的是一个类创建的实例,注意:
- 参数 c 的类型定义中,new()代表调用类的构造函数,他的类型也就是类创建实例后的实例的类型。
- return new c()这里使用传进来的类 c 创建一个实例并返回,返回的实例类型也就是函数的返回值类型。
所以通过这个定义,TypeScript 就知道,调用 create 函数,传入的和返回的值都应该是同一个类类型。