跳到主要内容

TS 完全支持 ES6 中的 class 关键字,同时增加了类型标注和一些用来表达类之间关系的语法。

类成员

字段

一个字段声明会创建一个可写的公开属性:

class Point {
x: number;
y: number;
}

const pt = new Point();
pt.x = 0;
pt.y = 0;

属性严格初始化

启用 --strictPropertyInitialization 会强制要求所有类字段初始化:

class BadGreeter {
name: string; // ERROR: `name` 属性没有初始化,且没有在构造器中被赋值
}

class GoodGreeter {
name: string;

constructor() {
this.name = 'hello';
}
}

如果你想在别的地方初始化,可以用断言操作符 ! 来忽略错误:

class OKGreeter {
name!: string; // 没有初始化,但是不会报错
}

readonly

使用 readonly 修饰符可以禁止字段在构造器外赋值:

class Greeter {
readonly name: string = 'world';

constructor(name?: string) {
this.name = otherName;
}

rename(name: string) {
this.name = name; // ERROR: `name` 是个只读属性
}
}
const g = new Greeter('Talaxy');
g.name = 'Allay'; // ERROR: `name` 是个只读属性

构造器

跟函数一样,构造器的形参可以类型标注、设默认值,以及重载:

class Point {
constructor(x: number, y: string);
constructor(s: string);
constructor(xs: any, y?: any) {
// 待实现...
}
}

但是,构造器:

  • 不能有类型参数,因为类型参数属于类声明;

  • 不能有返回值类型标注,构造器永远返回类实例类型。

和 JS 一样,如果是继承的类,在构造器中应当先调用 super() 再使用 this

方法

除了类型标注,TS 没有增加别的语法。在方法中应当通过 this 来访问实例:

class Point {
x = 10;
y = 10;

scale(n: number) {
this.x *= n;
y *= n; // ERROR: 无法找到 `y` ,是否指 `this.y` ?
}
}

访问器 Getter & Setter

TS 中的访问器有一些特别的推断规则:

  • 如果仅有 get 没有 set ,那么该属性会推断为 readonly

  • 如果 set 的形参类型是不明确的,那么会使用 get 的返回值类型;

  • Getter 和 Setter 必须有着相同的 可见性(访问控制)

class Thing {
_size = 0;

get size(): number {
return this._size;
}

set size(value: string | number | boolean) {
let num = Number(value);
// 限制了数值必须是有限的
if (!Number.isFinite(num)) {
this._size = 0;
return;
}
this._size = num;
}
}

索引签名

和对象类型一样,类可以设置索引签名:

class MyClass {
[s: string]: boolean | ((s: string) => boolean);

check(s: string) {
return this[s] as boolean;
}
}

类中的索引签名同样需要覆盖所有方法(除了构造器)的类型,所以定义起来有些困难。

类继承

implements 语句

可以用 implements 语句使一个类满足特定接口(接口可以为多个,用逗号隔开):

interface Fireable {
fire(): void;
}

interface Pingable {
ping(): void;
}

class Sonar implements Fireable, Pingable {
status: boolean = false;

fire() {
this.status = true;
}

ping() {
console.log('ping!');
}
}

需要注意的是,TS 只会检查类是否实现接口,而不会改变其原本的类型:

interface Checkable {
check(name: string): boolean;
log?(): void;
}

class NameChecker implements Checkable {
// ERROR: 's' 参数类型为 `any`
check(s) {
return s.toLowercase() === 'ok';
}
}

const checker = new NameChecker();
checker.log(); // ERROR: `NameChecker` 不存在 `log` 方法

上面这个例子中,TS 不会自行推断 check 方法的参数类型,只会检查你的实现方法是否满足接口。

extends 语句

和 JS 一样,类可以通过 extends 继承另一个类:

class Animal {
move() {
console.log('Moving');
}
}

class Dog extends Animal {
woof() {
console.log('Woof!');
}
}

const d = new Dog();
d.move(); // => 'Moving'
d.woof(); // => 'Woof!'

TS 同样可以覆盖基类方法:

class Bird extends Animal {
move(seconds?: number = 10) {
console.log(`Flying ${seconds}s`);
}
}

TS 会强制将继承后的类视为基类的子类型,继承后的类应当能当作基类一样使用。

如果想覆盖字段类型,可以用 declare 声明:

使用该特性可能需要条件,具体见 Type-only Field Declarations

class AnimalHouse {
resident: Animal;
constructor(animal: Animal) {
this.resident = animal;
}
}

class DogHouse extends AnimalHouse {
declare resident: Dog;

constructor(dog: Dog) {
super(dog);
}
}

访问控制

访问控制只会存在于 TS 类型检查,并不干扰 JS 运行时,具体见 警告

可以对类的成员设置访问控制,有这些访问控制类型:

  • public 始终可见;

  • protected 只能在自身或其子类的声明中访问;

  • private 只能在自身的声明中访问。

class Greeter {
private name = 'Talaxy';
protected getName() {
return this.name;
}
public greet() {
console.log('Hello, ' + this.getName());
}
}

const greeter = new Greeter();
greeter.name; // ERROR: 只能在 `Greeter` 中访问
greeter.getName(); // ERROR: 只能在 `Greeter` 或其子类中访问
greeter.greet(); // OK

如果没有设置访问控制,则默认为 public 等级。

访问控制提升

对于 protected 成员,可以在子类中提升为 public ;而 private 成员在子类中禁用,因此也无法提升访问控制的等级。

// ERROR: 私有属性 `name` 只属于 `Greeter` 类
class SpecialGreeter extends Greeter {
name = 'Hello';
// OK ,`getName` 访问控制等级提升为 `public`
getName() {
return this.name;
}
}

跨实例访问

TS 借鉴了 C#/C++ 中的跨实例访问的行为。

在类中可以直接访问同类实例的成员,即便是 private

class A {
private x = 10;

public sameAs(other: A) {
return other.x === this.x; // OK
}
}

而对于 protected ,类只能访问其同类或子类实例的 protected 成员:

class Base {
protected x = 1;
protected y = 1;
}

class Derived extends Base {
getX(obj: Base) {
return obj.x; // ERROR: 只能访问 `Derived` 实例的 `x` 属性
}
getY(obj: Derived) {
return obj.x; // OK
}
}

在上述例子中,Derived 类没有 Base 类的 protected 成员的访问权限。

静态成员

可以定义类自身的成员,即静态成员。且同样可以被继承、使用访问控制:

class MyClass {
protected static x = 0;
static printX() {
console.log(MyClass.x);
}
}

MyClass.x; // ERROR
MyClass.printX(); // => 0

因为类取自 Function 原型,静态成员不能与 Function 中的方法属性同名(如 name call apply 等)。

静态初始块

原文档静态初始块 - MDN

泛型类

类可以有类型参数,且在构造实例时会进行类型推断:

class Box<Type> {
contents: Type;
constructor(value: Type) {
this.contents = value;
}
}

const b = new Box('hello!');

但类型参数只能作用于非静态成员上,静态成员不能引用参数类型:

class Box<Type> {
static defaultValue: Type; // ERROR
}

运行时中的 this

原本 JS 的 this 在一些场合下使用会有难以理解的行为发生:

class MyClass {
name = 'MyClass';
getName() {
return this.name;
}
}
const c = new MyClass();
const obj = {
name: 'obj',
getName: c.getName,
};

console.log(obj.getName()); // => "obj"

在上面例子中,TS 没有能力告诉你上述代码是否符合你的预期。

和函数一样,可以在成员方法中限定 this 的类型来确保方法在正确的上下文中执行:

class MyClass {
name = 'MyClass';
getName(this: MyClass) {
return this.name;
}
}
const c = new MyClass();
c.getName(); // OK

const g = c.getName;
g(); // ERROR: 不能将 `void` 作为 `getName` 的 `this` 上下文

this 类型

一般情况下,可以直接使用类名作为同类类型引用:

class Box {
content = 'some items';
sameAs(box: Box) {
return this.content === box.content;
}
}

const box = new Box();
box.sameAs; // 函数类型为 `(box: Box) => boolean`

但出现继承的时候,比如:

class LargerBox extends Box {
content = 'more items';
}

const box = new LargerBox();
box.sameAs; // 函数类型依旧为 `(box: Box) => boolean`

上述例子的 box 实例的 sameAs 类型依旧是 (box: Box) => boolean 。如果期望类型为 (box: LargerBox) => boolean 的话,可以使用 this 作为类型:

class Box {
content = 'some items';
// 用 `this` 作为类型代替 `Box`
sameAs(box: this) {
return this.content === box.content;
}
}

const box = new LargerBox();
box.sameAs; // 函数类型为 `(box: LargerBox) => boolean`

如果在方法中返回 this ,TS 也会做同样的类型推断:

class Box {
content = 'some items';
send() {
return this;
}
}

class LargerBox extends Box {
content = 'more items';
}

const box = new LargerBox();
box.send; // 函数类型为 `() => LargerBox`

形参属性

TS 提供了一个便捷构造器的语法:

class Params {
constructor(
public readonly x: number,
protected y: number,
private z: number,
) {} // 方法体内不用写任何内容
}

const a = new Params(1, 2, 3);
a.x; // => 1
a.z; // => ERROR: 只能在 Params 内部访问

类表达式

同 JS 一样,支持类表达式:

const someClass = class<Type> {
content: Type;
constructor(value: Type) {
this.content = value;
}
};

const m = new someClass('Hello, world');

抽象类和成员

在 TS 中,类及其成员可以是抽象的。抽象类不能直接构造实例。

抽象属性或方法必须定义在抽象类中,用 abstract 修饰:

abstract class Base {
abstract getName(): string;

printName() {
console.log('Hello, ' + this.getName());
}
}

// `Derived` 必须实现所有 `Base` 的抽象成员
class Derived extends Base {
getName() {
return 'world';
}
}

const d = new Derived();
d.printName(); // => "Hello, world"

抽象类的构造签名

在函数中可能有传入构造器的情况,可以对类使用 typeof 来表示一个类型:

class ConcreteBase {
printName() {
console.log('Hello');
}
}

function greet(ctor: typeof ConcreteBase) {
const instance = new ctor();
instance.printName();
}

// `ConcreteBase` 的子类也可传入
greet(ConcreteBase); // => "Hello"

而对于抽象类,需要手动定义一个构造器类型,因为抽象类本身不具备构造实例的能力:

function greet(ctor: new () => Base) {
const instance = new ctor();
instance.printName();
}

greet(Derived); // OK
greet(Base); // => 不能将抽象的构造器类型赋值给非抽象的构造器类型

对于抽象类,TS 也会将其转为 JS 中实际的类,因此也可以像非抽象类一样定义静态成员。

类之间的关系

通常情况下,TS 是根据实际构成来比较类之间关系的。比如下面这个例子:

class Person {
name: string;
age: number;
}

class Employee {
name: string;
age: number;
salary: number;
}

const p: Person = new Employee(); // OK

参考

Classes - TypeScript