类
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