简介 属性的类型 类的属性可以在顶层声明,也可以在构造方法内部声明。
class Point {
x:number;
y:number;
}
strictPropertyInitialization,只要打开(默认是打开的),就会检查属性是否设置了初值,如果没有就报错。
如果类的顶层属性不赋值,就会报错。如果不希望出现报错,可以使用非空断言。
class Point {
x!: number;
y!: number;
}
readonly 修饰符
如果两个地方都设置了只读属性的值,以构造方法为准
class A {
readonly id:string = 'foo';
constructor() {
this.id = 'bar'; // 正确
}
}
方法的类型 类的方法就是普通函数,类型声明方式与函数一致。 构造方法不能声明返回值类型,否则报错,因为它总是返回实例对象。
class B {
constructor():object { // 报错
// ...
}
}
存取器方法
TypeScript 对存取器有以下规则。
(1)如果某个属性只有get方法,没有set方法,那么该属性自动成为只读属性。
(2)TypeScript 5.1 版之前,set方法的参数类型,必须兼容get方法的返回值类型,否则报错。
class C {
_name = '';
get name():string {
return this._name;
}
set name(value:number|string) {
this._name = String(value);
}
}
(3)get方法与set方法的可访问性必须一致,要么都为公开方法,要么都为私有方法。
属性索引
如果一个对象同时定义了属性索引和方法,那么前者必须包含后者的类型。
class MyClass {
[s:string]: boolean | (() => boolean);
f() {
return true;
}
}
类的 interface 接口
类使用 implements 关键字,表示当前类满足这些外部类型条件的限制。 interface 只是指定检查条件,如果不满足这些条件就会报错。它并不能代替 class 自身的类型声明。
interface A {
get(name:string): boolean;
}
class B implements A {
get(s) { // s 的类型是 any
return true;
}
}
类可以定义接口没有声明的方法和属性。
interface Point {
x: number;
y: number;
}
class MyPoint implements Point {
x = 1;
y = 1;
z:number = 1;
}
implements关键字后面,不仅可以是接口,也可以是另一个类。这时,后面的类将被当作接口。
class Car {
id:number = 1;
move():void {};
}
class MyCar implements Car {
id = 2; // 不可省略
move():void {}; // 不可省略
}
interface 描述的是类的对外接口,也就是实例的公开属性和公开方法,不能定义私有的属性和方法。
实现多个接口
类可以实现多个接口(其实是接受多重限制),每个接口之间使用逗号分隔。
class Car implements MotorVehicle, Flyable, Swimmable {
// ...
}
同时实现多个接口并不是一个好的写法,容易使得代码难以管理,可以使用两种方法替代。
第一种方法是类的继承。
class Car implements MotorVehicle {
}
class SecretCar extends Car implements Flyable, Swimmable {
}
第二种方法是接口的继承。
interface SuperCar extends MotorVehicle, Flyable, Swimmable {
// ...
}
class SecretCar implements SuperCar {
// ...
}
发生多重实现时(即一个接口同时实现多个接口),不同接口不能有互相冲突的属性。
类与接口的合并
TypeScript 不允许两个同名的类,但是如果一个类和一个接口同名,那么接口会被合并进类。
class A {
x:number = 1;
}
interface A {
y:number;
}
let a = new A();
a.y = 10;
a.x // 1
a.y // 10
Class 类型
实例类型
TypeScript 的类本身就是一种类型,但是它代表该类的实例类型,而不是 class 的自身类型。 对于引用实例对象的变量来说,既可以声明类型为 Class,也可以声明类型为 Interface,因为两者都代表实例对象的类型。
interface MotorVehicle {
}
class Car implements MotorVehicle {
}
// 写法一
const c1:Car = new Car();
// 写法二
const c2:MotorVehicle = new Car();
作为类型使用时,类名只能表示实例的类型,不能表示类的自身类型。
class Point {
x:number;
y:number;
constructor(x:number, y:number) {
this.x = x;
this.y = y;
}
}
// 错误
function createPoint(
PointClass:Point,
x: number,
y: number
) {
return new PointClass(x, y);
}
TypeScript 有三种方法可以为对象类型起名:type、interface 和 class。
类的自身类型
要获得一个类的自身类型,一个简便的方法就是使用 typeof 运算符。 类的自身类型可以写成构造函数的形式。 构造函数也可以写成对象形式
function createPoint(PointClass: typeof Point, x: number, y: number): Point {
return new PointClass(x, y);
}
function createPoint(
PointClass: new (x: number, y: number) => Point,
x: number,
y: number
): Point {
return new PointClass(x, y);
}
function createPoint(
PointClass: {
new (x: number, y: number): Point;
},
x: number,
y: number
): Point {
return new PointClass(x, y);
}
结构类型原则
Class 也遵循“结构类型原则”。一个对象只要满足 Class 的实例结构,就跟该 Class 属于同一个类型。
class Foo {
id!:number;
}
function fn(arg:Foo) {
// ...
}
const bar = {
id: 10,
amount: 100,
};
fn(bar); // 正确
如果两个类的实例结构相同,那么这两个类就是兼容的,可以用在对方的使用场合。
class Person {
name: string;
age: number;
}
class Customer {
name: string;
}
// 正确
const cust:Customer = new Person();
如果某个对象跟某个 class 的实例结构相同,TypeScript 也认为两者的类型相同。
class Person {
name: string;
}
const obj = { name: 'John' };
const p:Person = obj; // 正确
运算符instanceof不适用于判断某个对象是否跟某个 class 属于同一类型。
空类不包含任何成员,任何其他类都可以看作与空类结构相同。因此,凡是类型为空类的地方,所有类(包括对象)都可以使用。
class Empty {}
function fn(x:Empty) {
// ...
}
fn({});
fn(window);
fn(fn);
确定两个类的兼容关系时,只检查实例成员,不考虑静态成员和构造方法。
class Point {
x: number;
y: number;
static t: number;
constructor(x:number) {}
}
class Position {
x: number;
y: number;
z: number;
constructor(x:string) {}
}
const point:Point = new Position('');
如果类中存在私有成员(private)或保护成员(protected),那么确定兼容关系时,TypeScript 要求私有成员和保护成员来自同一个类,这意味着两个类需要存在继承关系。
// 情况一
class A {
private name = 'a';
}
class B extends A {
}
const a:A = new B();
// 情况二
class A {
protected name = 'a';
}
class B extends A {
protected name = 'b';
}
const a:A = new B();
类的继承
类(这里又称“子类”)可以使用 extends 关键字继承另一个类(这里又称“基类”)的所有属性和方法。
变量a的类型是基类,但是可以赋值为子类的实例
class A {
greet() {
console.log("Hello, world!");
}
}
class B extends A {}
const b = new B();
b.greet(); // "Hello, world!"
const a: A = b;
a.greet();
子类可以覆盖基类的同名方法。
class B extends A {
greet(name?: string) {
if (name === undefined) {
super.greet();
} else {
console.log(`Hello, ${name}`);
}
}
}
子类的同名方法不能与基类的类型定义相冲突。
如果基类包括保护成员(protected修饰符),子类可以将该成员的可访问性设置为公开(public修饰符),也可以保持保护成员不变,但是不能改用私有成员(private修饰符)
class A {
protected x: string = '';
protected y: string = '';
protected z: string = '';
}
class B extends A {
// 正确
public x:string = '';
// 正确
protected y:string = '';
// 报错
private z: string = '';
}
extends关键字后面不一定是类名,可以是一个表达式,只要它的类型是构造函数
// 例一
class MyArray extends Array<number> {}
// 例二
class MyError extends Error {}
// 例三
class A {
greeting() {
return 'Hello from A';
}
}
class B {
greeting() {
return 'Hello from B';
}
}
interface Greeter {
greeting(): string;
}
interface GreeterConstructor {
new (): Greeter;
}
function getGreeterBase():GreeterConstructor {
return Math.random() >= 0.5 ? A : B;
}
class Test extends getGreeterBase() {
sayHello() {
console.log(this.greeting());
}
}
对于那些只设置了类型、没有初值的顶层属性,有一个细节需要注意。
如果编译设置的target设成大于等于ES2022,或者useDefineForClassFields设成true,那么下面代码的执行结果是不一样的。
interface Animal {
animalStuff: any;
}
interface Dog extends Animal {
dogStuff: any;
}
class AnimalHouse {
resident: Animal;
constructor(animal: Animal) {
this.resident = animal;
}
}
class DogHouse extends AnimalHouse {
resident: Dog;
constructor(dog: Dog) {
super(dog);
}
}
const dog = {
animalStuff: "animal",
dogStuff: "dog",
};
const dogHouse = new DogHouse(dog);
console.log(dogHouse.resident); // undefined
可访问性修饰符
public
类的属性和方法默认都是外部可访问的。
private
private修饰符表示私有成员,只能用在当前类的内部,类的实例和子类都不能使用该成员。
子类不能定义父类私有成员的同名成员。
如果在类的内部,当前类的实例可以获取私有成员。private定义的私有成员,并不是真正意义的私有成员。
构造方法也可以是私有的,这就直接防止了使用new命令生成实例对象,只能在类的内部创建实例对象。
class Singleton {
private static instance?: Singleton;
private constructor() {}
static getInstance() {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
}
const s = Singleton.getInstance();
protected
protected修饰符表示该成员是保护成员,只能在类的内部使用该成员,实例无法使用该成员,但是子类内部可以使用。
class A {
protected x = 1;
}
class B extends A {
getX() {
return this.x;
}
}
const a = new A();
const b = new B();
a.x // 报错
b.getX() // 1
子类不仅可以拿到父类的保护成员,还可以定义同名成员。
class B extends A {
x = 2;
}
在类的外部,实例对象不能读取保护成员,但是在类的内部可以。
class A {
protected x = 1;
f(obj:A) {
console.log(obj.x);
}
}
const a = new A();
a.x // 报错
a.f(a) // 1
实例属性的简写形式
构造方法的参数x前面有public修饰符,这时 TypeScript 就会自动声明一个公开属性x,不必在构造方法里面写任何代码,同时还会设置x的值为构造方法的参数值。
class Point {
x:number;
y:number;
constructor(x:number, y:number) {
this.x = x;
this.y = y;
}
}
class Point {
constructor(
public x:number,
public y:number
) {}
}
const p = new Point(10, 10);
p.x // 10
p.y // 10
静态成员 # **
类的内部可以使用static关键字,定义静态成员。
静态成员是只能通过类本身使用的成员,不能通过实例对象使用。
静态属性x前面有private修饰符,表示只能在MyClass内部使用
class MyClass {
private static x = 0;
}
MyClass.x // 报错
public和protected的静态成员可以被继承。
class A {
public static x = 1;
protected static y = 1;
}
class B extends A {
static getY() {
return B.y;
}
}
B.x // 1
B.getY() // 1
泛型类
类也可以写成泛型,使用类型参数。
class Box<Type> {
contents: Type;
constructor(value:Type) {
this.contents = value;
}
}
const b:Box<string> = new Box('hello!');
静态成员不能使用泛型的类型参数。
class Box<Type> {
static defaultContents: Type; // 报错
}
抽象类,抽象成员
TypeScript 允许在类的定义前面,加上关键字abstract,表示该类不能被实例化,只能当作其他类的模板。这种类就叫做“抽象类”(abstract class)。
abstract class A {
id = 1;
}
class B extends A {
amount = 100;
}
const b = new B();
b.id // 1
b.amount // 100
抽象类可以继承其他抽象类。
抽象类的内部可以有已经实现好的属性和方法,也可以有还未实现的属性和方法。后者就叫做“抽象成员”(abstract member),即属性名和方法名有abstract关键字,表示该方法需要子类实现。如果子类没有实现抽象成员,就会报错。
abstract class A {
abstract foo:string;
bar:string = '';
}
class B extends A {
foo = 'b';
}
抽象方法
abstract class A {
abstract execute():string;
}
class B extends A {
execute() {
return `B executed`;
}
}
(1)抽象成员只能存在于抽象类,不能存在于普通类。
(2)抽象成员不能有具体实现的代码。也就是说,已经实现好的成员前面不能加abstract关键字。
(3)抽象成员前也不能有private修饰符,否则无法在子类中实现该成员。
(4)一个子类最多只能继承一个抽象类。
this 问题
TypeScript 允许函数增加一个名为this的参数
class A {
name = 'A';
getName(this: A) {
return this.name;
}
}
const a = new A();
const b = a.getName;
b() // 报错
TypeScript 提供了一个noImplicitThis编译选项。如果打开了这个设置项,如果this的值推断为any类型,就会报错。
在类的内部,this本身也可以当作类型使用,表示当前类的实例对象。
class Box {
contents:string = '';
set(value:string):this {
this.contents = value;
return this;
}
}
this类型不允许应用于静态成员。
有些方法返回一个布尔值,表示当前的this是否属于某种类型。这时,这些方法的返回值类型可以写成this is Type的形式,其中用到了is运算符。
class FileSystemObject {
isFile(): this is FileRep {
return this instanceof FileRep;
}
isDirectory(): this is Directory {
return this instanceof Directory;
}
// ...
}