Skip to main content

Class

TypeScript完全支持class关键字,像其他JavaScript语言特性一样,TypeScript添加了类型注释和其他语法,让你可以表达类和其他类型之间的关系。

类成员

空类,这种类目前还不太有用,需要结合类成员使用。

class Point { }

属性声明在一个类上创建一个公开可写的属性:

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

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

类型注解是可选的,如果未指定,它将默认为隐式的any类型:

class Point {
// Member 'x' implicitly has an 'any' type, but a better type may be inferred from usage.
x;
}

属性也可以有初始值,当类被实例化时,这些初始值将自动绑定:

class Point {
x = 0;
y = 0;
}

const pt = new Point();
console.log(pt.x, pt.y);

若未指定类属性类型,初始值可用来进行类型推断,如上推断xnumber类型,如下操作将会报错:

// Type 'string' is not assignable to type 'number'.ts(2322)
pt.x = 'a'

strictPropertyInitialization

strictPropertyInitialization选项设置控制着类字段是否需要在构造函数中进行初始化:

// tsconfig.json 
{
compilerOptions: {
strictPropertyInitialization: true
}
}

class Point {
// Property 'x' has no initializer and
// is not definitely assigned in the constructor.
x: number;
constructor() {
this.x = 10;
}
}

属性需要在构造函数本身中进行初始化。TypeScript不会分析从构造函数调用的方法来检测初始化,因为派生类可能会重写这些方法并且未能初始化成员。

如果的确需要通过构造函数之外的方式明确的初始化一个属性,可以使用明确赋值断言运算符!

class TestClass {
a!: number;

constructor(
a?: number,
) {
a && (this.a = a)
}
}

只读属性

属性可以以readonly修饰符为前缀,这样可以防止在构造函数之外对属性进行赋值:

class Point {
readonly x: number = 10;

constructor(x?: number) {
if (x !== undefined) {
this.x = x;
}
}

setX(x: number) {
// Cannot assign to 'x' because it is a read-only property
this.x = x;
}
}

const pt = new Point(20);
// Cannot assign to 'x' because it is a read-only property
pt.x = 30

构造函数

类的构造函数跟普通函数很相似,可以添加类型注解、默认值、可选参数:

class Point {
x: number;
y: number;

constructor(x: number = 0, y: number = 0) {
this.x = x;
this.y = y;
}
}

const pt = new Point();
console.log(pt.x, pt.y); // 0 0

const p1 = new Point(10, 20);
console.log(p1.x, p1.y); // 10 20

除此之外,还可进行重载:

class Point {
x: number | string;
y?: number;

// overload declare
constructor(x: number, y: number);
constructor(s: string);

// implementation
constructor(xs: number | string, y?: number) {
if (typeof xs === 'number' && typeof y === 'number') {
this.x = xs;
this.y = y;
} else {
this.x = xs
}
}
}

const pt = new Point(10, 20);
console.log(pt);

const p1 = new Point('hello');
console.log(p1);

类构造函数和函数签名的区别:

  • 构造函数没有类型参数。
  • 构造函数没有返回类型注解,类实例的类型始终是返回值。

super

在JavaScript中,如果有一个基类,需要在构造函数体中先调用super(),然后再使用任何this.成员:

class Base {
k = 100;
}

class Derived extends Base {
constructor() {
// 'super' must be called before accessing 'this' in the constructor of a derived class.
console.log(this.k);
}
}

在JavaScript中忘记调用super是一个容易犯的错误,但是TypeScript会在必要的时候提醒您。

方法

类上的函数属性被称为方法,方法可以使用与函数和构造函数相同的类型注解:

class Point {
x = 10;
y = 10;

scale(n: number): void {
this.x *= n;
this.y *= n;
}
}

除了标准的类型注解之外,TypeScript在方法中没有引入任何其他新内容。

在方法体内部,仍然必须通过this关键字访问字段和其他方法。方法体中的未限定名称始终将引用封闭作用域中的某个内容:

let x: number = 10;

class C {
x: string = 'hello';

m() {
// This is trying to modify 'x' from line 1, not the class property
x = 100
}
}

getters/setters

TypeScript中,类也可以有访问器:

class C {
_length = 0;

get length() {
return this._length;
}

set length(value) {
this._length = value;
}
}

成员属性名不能和getter函数重名:

class UserInfo {
user: string;

constructor(user: string) {
this.user = user
}

// Duplicate identifier 'user'.
get user() {
return this.user
}
}

TypeScript针对访问器有一些特殊的推断规则:

  • 如果只有get而没有set,那么此属性会被自动的认为是readonly
  • 如果set函数的参数类型不明确,那么将会从对应的set函数的返回值推断。
  • getset必须有相同的成员可见性。

自TypeScript 4.3版本开始,可以为访问器的setget定义不同的类型:

class Thing {
_size = 0;

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

set size(value: number | string | boolean) {
const num = Number(value);

if (!Number.isFinite(num)) {
this._size = num;
return;
}

this._size = num
}
}

索引签名

类也可以声明索引签名,其工作方式跟对象中的相同:

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

constructor() {
this.a = true
this.b = false
}

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

const o = new MyClass();
console.log(o.check('a')); // true
// c属性不属于o对象的属性!!!
console.log(o.check('c')); // undefined

由于索引签名类型需要捕获方法的类型,且属于动态属性名,因此很难对方法的类型进行准确的推断。

那么如何进行改进呢?将索引数据存储在类实例本身之外的其他位置:

class MyClass {
data: Map<string, boolean | ((s: string) => boolean)>;

constructor() {
this.data = new Map<string, boolean | ((s: string) => boolean)>();
this.data.set("a", true);
this.data.set("b", false);
}

check(s: string): boolean {
const value = this.data.get(s);
if (typeof value === "function") {
return value(s);
} else {
return value as boolean;
}
}
}

const o = new MyClass();
o.data.set("c", (s: string) => s.length > 0);
console.log(o.check("a")); // true
console.log(o.check("b")); // false
console.log(o.check("c")); // false

implements

面向对象中,所有的公共方法都需要由接口进行规范,类进行实现。

在TypeScript中,可以使用implements子句来检查一个类是否满足特定的接口,以确保类遵循接口的定义并实现接口中声明的所有属性和方法。如果一个类没有正确实现该接口,将会发出错误提示:

interface PingPong {
ping(): void
}

// type PingPong = {
// ping(): void
// }

class Sonar implements PingPong {
ping() {
console.log('ping!');
}
}

// Class 'Ball' incorrectly implements interface 'PingPong'.
class Ball implements PingPong {
pong() {

}
}

当然,也可以同时实现多个接口:

interface A {
a(): void
}

interface B {
b(): void
}

class C implements A,B {
a(): void {

}

b(): void {

}
}

implements子句只是一个检查,确保该类可以被视为接口类型的一种方式,它并不会改变类或其方法的类型。一个常见的错误是误以为implements子句会改变类的类型,但实际上它并不会改变:

interface checkable {
check(name: string): boolean
}

class NameChecker implements checkable {
// Parameter 'name' implicitly has an 'any' type.
// name is NOT string type
check(s): boolean {
return s === 'admin'
}
}

如上示例,我们可能期望s的类型会受到check函数的name: string参数的影响。然而实际情况是,并不会——implements子句不会改变对类主体的检查方式,也不会改变其类型的推断,因此这里s的类型是any.

相似地,实现一个带有可选属性的接口并不会创建该属性:

interface A {
x: number;
y?: number;
}

class C implements A {
x: number = 0;
}

const oo = new C();
// Property 'y' does not exist on type 'C'.
oo.y = 1

extends

类可以继承自基类。派生类拥有基类的所有属性和方法,并且还可以定义额外的成员。

class Animal {
name: string = '';

constructor(name: string) {
this.name = name
}

move() {
console.log('move');
}
}

class Dog extends Animal {
age: number;

constructor(name: string, age: number) {
super(name)
this.age = age
}

bark() {
console.log('bark');
}
}

const d = new Dog('dog', 2);
d.bark();
d.move();

方法重载

派生类可以重写基类的属性,也可是使用super.语法访问基类的方法。由于JavaScript 类是一个简单的查找对象,所以没有super字段的概念,在派生类中无法直接访问基类中的属性。

在TypeScript中,会强制使得派生类是其基类的子类型。

class Base {
greet() {
console.log('hello');
}
}

class Derived extends Base {
greet(name?: string) {
if (name === undefined) {
super.greet()
} else {
console.log(`hello ${name.toUpperCase()}`);
}
}
}

const ds = new Derived();
ds.greet();
ds.greet("ts");

派生类遵循其基类的契约,通过基类引用来引用派生类实例是非常常见(而且始终合法)的操作:

const dds: Base = ds
dds.greet()

如果派生类没有遵循基类的契约,可能会导致不符合预期的行为或错误的结果:

class Base {
greet() {
console.log("Hello, world!");
}
}

class Derived extends Base {
// Property 'greet' in type 'Derived' is not assignable
// to the same property in base type 'Base'.
greet(name: string) {
console.log(`Hello, ${name.toUpperCase()}`);
}
}

如上,派生类中greet方法参数与基类中该方法不兼容,会产生错误。

类型字段声明

当目标版本大于等于ES2022或者使用了 useDefineForClassFields 选项时,类字段会在父类构造函数完成后进行初始化,覆盖父类设置的任何值。这可能会在只想为继承的字段重新声明一个更准确的类型时造成问题。为了处理这些情况,可以使用declare关键字告诉 TypeScript 此字段声明不会在运行时产生任何影响:

interface Animal {
dateOfBirth: any;
}

interface Dog extends Animal {
breed: any;
}

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

class DogHouse extends AnimalHouse {
// 重新声明resident为一个更准确的类型
declare resident: Dog;
constructor(dog: Dog) {
super(dog);
}
}

初始化顺序

JavaScript类的初始化顺序在某些情况下可能会令人惊讶。考虑如下示例:

class Base {
name = 'base'
constructor() {
console.log('base name', this.name);

}
}

class Derived extends Base {
name = 'derived'
}

new Derived() // base name base

TypeScript中,初始化顺序由JavaScript定义:

  1. 基类字段初始化
  2. 基类构造函数执行
  3. 派生类字段初始化
  4. 派生类构造函数执行

这意味着基类构造函数在自身的构造函数期间看到了自己的name值,因为派生类字段的初始化尚未执行。

继承内置类型

在 ES2015 中,如果构造函数返回一个对象,它会隐式地替换super(...)调用者中的this 值。为了生成的构造函数代码能够捕获可能的super(...)返回值并将其替换为 this,这是必要的。

class Parent {
name: string;
constructor() {
this.name = 'Parent'
}
}

class Child extends Parent {
declare name: string;
constructor() {
super()
return {
name: 'Child'
}
}
}

const res = new Child()
console.log(res); // { name: 'Child' }

这样一来,对 ErrorArray和其他一些内置类型进行子类化可能不再按预期工作,这是因为ErrorArray等构造函数使用ECMAScript 6的new.target来调整原型链。但是,在 ECMAScript 5 中,在调用构造函数时无法确保new.target的值,其他低级别编译器通常默认具有相同的限制。

考虑代码:

class MsgError extends Error {
constructor(m: string) {
super(m);
}
sayHello() {
console.log("hello " + this.message);
}
}

你可能会发现:

  1. 在构造这些子类的实例对象上,方法可能是未定义的,因此调用sayHello方法将导致错误。
  2. 子类的实例对象与它们的构造函数实例之间的instanceof关系将会被破坏,所以 (new MsgError()) instanceof MsgError将返回false

如何解决?可以在任何super(...)调用之后手动调整原型链:

class MsgError extends Error {
constructor(m: string) {
super(m);
// Set the prototype explicitly.
Object.setPrototypeOf(this, MsgError.prototype);
}
sayHello() {
console.log("hello " + this.message);
}
}

然而,MsgError的任何子类都必须手动设置原型链,并且对于不支持 Object.setPrototypeOf的运行时,可以尝试使用__proto__

封装性

class中,所有的属性都应该是私有的,如果需要获取属性和设置属性,应该由公共的gettersetter方法完成,并且所有的公共方法都需要接口进行规范。

与大多数OOP语言类似,TypeScript可以控制某些方法或属性对类外部的代码是否可见。其中包括publicprotectedprivate等。

public

默认情况下,类成员的可见性是public,公共成员可以在任何地方访问:

class Greeter {
public greet() {
console.log("Hello, world!");
}
}

const greeter = new Greeter();
greeter.greet();

因为public已经是默认的可见性修饰符,所以通常不需要在类成员上写它,但出于样式和可读性的原因,你可能选择这样做。

protected

protected成员只对声明它们的类的子类可见。

class Greeter {
public greet() {
console.log("Hello, world!");
}

protected getName() {
return 'Tom';
}
}

class SubGreeter extends Greeter {
public howdy() {
// it's OK to access protected member here
console.log("Howdy, " + this.getName());
}
}

const greeter = new SubGreeter();
greeter.greet();
greeter.howdy();
// Property 'getName' is protected and only accessible within
// class 'Greeter' and its subclasses.
greeter.getName();

暴露protected成员

派生类需要遵循其基类的契约,但可以选择公开具有更多功能的基类子类型。这包括将protected成员公开为public

class Base {
protected m = 10;
}

class Derived extends Base {
// proteect -> public
m = 15;
}

const dd = new Derived();
console.log(dd.m); // 15

如上,Derived已经可以自由地读取和写入m,因此这并不会实质性地改变此情况的“安全性”。在派生类中,如果这种公开不是有意的,需要小心地重复使用protected修饰符。

跨层级保护访问

不同的面向对象编程语言在通过基类引用访问受保护成员是否合法方面存在分歧:

class Base {
protected m = 1;
}

class Derived1 extends Base {
protected m = 2;
}

class Derived2 extends Base {
f1(other: Derived1) {
// Property 'm' is protected and only accessible within
// class 'Derived1' and its subclasses.
other.m = 10;
}

f2(other: Derived2) {
other.m = 20;
}
}

如上,只有从Derived2的子类中才能合法地访问Derived2中的xDerived1不是其子类。

private

privateprotected类似,但是只有类本身才能访问其成员:

class Base {
private x = 1;
}

class Derived extends Base {
getX() {
// Property 'x' is private and only accessible within class 'Base'.
return this.x;
}
}

const px = new Base();
// Property 'x' is private and only accessible within class 'Base'.
console.log(px.x);

由于私有成员对派生类不可见,因此派生类无法增加它们的可见性:

class Base {
private x = 1;
}

class Derived extends Base {
x = 10;
}

跨实例私有访问

不同的面向对象编程语言在同一类的不同实例是否可以访问彼此的私有成员上存在分歧。Java、C#、C++、Swift 和 PHP 等语言允许这样做,但Ruby不允许。

在TypeScript中,允许跨实例私有访问:

class A {
private x = 1;

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

私有成员访问

  • 与TypeScript类型系统的其他方面一样,privateprotected只在类型检查期间强制执行。这意味着JavaScript运行时的构造,比如in操作符或简单的属性查找,仍然可以访问私有或受保护的成员:
class MySafe {
private secretKey = 1
}

/*
编译后结果:
var MySafe = (function () {
function MySafe() {
this.secretKey = 1;
}
return MySafe;
}());

const s = new MySafe()
console.log(s.secretKey)
*/
  • 在类型检查期间,private也允许使用方括号表示法进行访问。这使得使用private声明的字段在诸如单元测试等方面更容易访问,但缺点是这些字段只是软私有的,并不能严格强制隐私。
class MySafe {
private secretKey = 1
}

const s = new MySafe()
console.log(s['secretKey']);
  • 与 TypeScript的private不同,JavaScript的私有字段#在编译后仍然保持私有,并且不提供前面访问方式,使其成为真正的私有字段。
class MySafe {
#secretKey = 1
}

/*
编译为最新ES标准:
class MySafe {
#secretKey = 1
}

编译为ES2021或更早标准:
var _MySafe_secretKey;
class MySafe {
constructor() {
_MySafe_secretKey.set(this, 1);
}
}
_MySafe_secretKey = new WeakMap();
*/

如果需要保护类中的值免受恶意操作者的攻击,应该使用提供真正运行时隐私保护的机制,例如闭包、WeakMap或私有属性。但是,运行时的这些额外隐私检查可能会影响性能。

static

在TypeScript中,类也可以有静态成员,这些成员不与类的特定实例关联,因此无法访问实例的成员,静态成员在类的实例生成之前已经定义。

静态成员可通过类构造函数对象本身进行访问:

class Base {
static x = 0;

static printX() {
console.log(Base.x);
}
}

console.log(Base.x);
Base.printX();

静态成员也可以使用相同的publicprotectedprivate可见性修饰符:

class Base {
private static x = 0;

static printX() {
console.log(Base.x);
}
}

// Property 'x' is private and only accessible within class 'Base'.
console.log(Base.x);
Base.printX();

静态成员也可以被继承:

class Base {
static getGreeting() {
return 'hello world'
}
}

class Derived extends Base { }

console.log(Derived.getGreeting()); // hello world

特殊静态成员名称

通常情况下,重写函数原型(Function prototype)的属性是不安全/不可行的。由于类本身是可以使用new 调用的函数,因此某些静态名称不能使用。函数属性(如namelengthcall)不能用作静态成员的定义:

class Base {
// Static property 'name' conflicts with built-in property
// 'Function.name' of constructor function 'S'.
static name = 'name is S'
}

静态代码块

静态代码块允许编写一系列具有自己作用域的语句,可以访问包含类中的私有字段。这意味着我们可以编写具有编写语句的所有功能、无变量泄漏并完全访问类内部的初始化代码。

class Base {
private static num: number;

constructor() {
console.log('constructor');
}

get count() {
return Base.num
}

static {
console.log('static block');
Base.num = Math.random() > 0.2 ? 1 : 2
}
}

const bb = new Base()
console.log(bb.count);

/*
output:
static block
constructor
1 or 2
*/

静态代码块在类被实例化之前、类加载的时候执行,并且只执行一次,主要用于在类加载时执行一些初始化操作或设置静态成员。

过多或复杂的静态代码块可能会导致类加载变慢,而且可能会增加代码的复杂性。因此,只有在确实有需要时,才应该使用静态代码块。

private constructor

final是某些语言用来标记类为不可扩展或方法为不可重写的关键字,尽管TypeScript不支持类或方法的final关键字,但可以很容易地模拟类似的功能。TypeScript中,可将constructor标记为private,可限制类的拓展性,如不可被继承、不可被实例化:

class MessageQueue {
private messages: string[] = [];
private constructor(messages: string[]) {
this.messages = messages;
}
}

// Constructor of class 'MessageQueue' is private and
// only accessible within the class declaration.
new MessageQueue()

// Cannot extend a class 'MessageQueue'.
// Class constructor is marked as private.
class MyMessageQueue extends MessageQueue {
constructor() {
super([]);
}
}

但这样就完全限制了类的拓展性,有时候确实希望类不可被继承,但可以实例化,这种情况下可使用如下方式实现:

class MessageQueue {
private messages: string[] = [];
private constructor(messages: string[]) {
this.messages = messages;
}

static create(messages: string[]) {
return new MessageQueue(messages);
}
}

MessageQueue.create(['hello', 'world'])

静态成员类型参数

考虑如下示例:

class Box<T> {
// Static members cannot reference class type parameters.
static defaultValue: T;
}

类型在运行时总是被完全擦除的。在运行时,只有一Box.defaultValue属性槽位。这意味着设置 Box<string>.defaultValue(如果可能的话)也会改变Box<number>.defaultValue - 这并不好。泛型类的静态成员永远不能引用类的类型参数。

运行时this

TypeScript不会改变JavaScript 的运行时行为,而JavaScript在运行时具有一些特殊的行为,这是众所周知的。其中常见的是对this的处理,TypeScript提供了一些方法来减轻或防止这种类型的错误。

箭头函数

如果有一个函数经常以失去其this上下文的方式调用,使用箭头函数属性而不是方法定义可能是有意义的:

class Base {
public name: string = 'base';

getName = (): string => {
return this.name;
}
}

const bb = new Base();
const { getName } = bb;
console.log(getName()); // base

这种方法有一些权衡之处:

  • 在运行时,无论是否使用TypeScript检查的代码,this值都是正确的。
  • 这会使用更多的内存,因为每个类实例将拥有自己的这种方式定义的函数的副本。
  • 在派生类中无法使用super.getName(),因为原型链中没有获取基类方法的条目。

this参数

在TypeScript中,方法或函数定义中名为this的初始参数具有特殊含义。这些参数在编译过程中被擦除:

// TypeScript input with 'this' parameter
function fn(this: SomeType, x: number) {
/* ... */
}

// JavaScript output
function fn(x) {
/* ... */
}

TypeScript检查调用带有this参数的函数是否在正确的上下文中进行。我们可以在方法定义中添加this参数,以静态地强制要求正确调用该方法,而无需使用箭头函数:

class Base {
public name: string = 'base';

getName(this: Base): string {
return this.name;
}
}

const bb = new Base();
console.log(bb.getName()); // base

const gg = bb.getName
// The 'this' context of type 'void' is not assignable to method's 'this' of type 'Base'.
console.log(gg());

这种方法具有与箭头函数方法相反的权衡:

  • JavaScript调用者可能仍然错误地使用类方法,而不自知。
  • 每个类定义只分配一个函数,而不是每个类实例一个函数。
  • 基类方法仍然可以通过super调用。

this类型

在类中,有一个特殊的类型称为this,它动态地指代当前类的类型:

class Base {
contents: string = '';

// (method) Base.set(value: string): this
set(value: string) {
this.contents = value;
return this;
}
}

class Derived extends Base {
clear() {
this.contents = '';
}
}

const bb = new Base();
const rr = bb.set('hello'); // Base

const dd = new Derived();
const re = dd.set('hello'); // Derived

也可以在参数类型注解中使用this,但有一点要注意,如果有一个派生类,派生类继承相同的方法只能接受相同派生类的实例:

class Base {
content: string = '';
// this -> Base Instance
sameAs(other: this) {
return other.content === this.content;
}
}

class Derived extends Base {
otherContent: string = '';

// this -> Derived Instance
// sameAs(other: this) {
// return other.content === this.content;
// }
}

const bb = new Base();
const dd = new Derived();
// Argument of type 'Base' is not assignable to parameter of type 'Derived'.
dd.sameAs(bb); // Error

基于this的类型守卫

一种在类方法中使用this参数来缩小类型范围的技术。通过在方法中使用条件语句并基于this的类型进行判断,可以在特定条件下改变当前对象的类型。

class Animal {
isDog(): this is Dog {
return this instanceof Dog;
}
}

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

class Cat extends Animal {
meow() {
console.log("Meow!");
}
}

const dd = new Dog();
const cc = new Cat();

if (dd.isDog()) {
dd.bark();
}

if (cc.isDog()) {
cc.bark();
}

参数属性

TypeScript为将构造函数参数转换为具有相同名称和值的类属性提供了特殊语法。这些被称为参数属性,可以通过在构造函数参数前面加上可见性修饰符publicprivateprotectedreadonly来创建。生成的字段将具有相应的修饰符:

class Params {
constructor(
public readonly x: number,
protected y: number,
private z: number,
) {
// No body necessary
/**
* 自动执行了以下代码
* this.x = x;
* this.y = y;
* this.z = z;
*/
}
}

const params = new Params(1, 2, 3);
console.log(params.x); // 1
console.log(params.y); // error
console.log(params.z); // error

类表达式

类表达式与类声明非常相似。唯一的区别在于类表达式不需要名称,我们可以通过绑定到它们的标识符来引用它们:

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

new Base('hello')

// ---------------------
class Base <T>
content: T;
constructor(value: T) {
this.content = value;
}
}

const baseClass = Base;

new baseClass('hello'); // hello

鸭子类型

鸭子可以走、叫,只要有走和叫的行为都可以看成一只鸭子,鸭子变成了一种类型。

抽象类与成员

抽象方法或抽象字段是指没有提供实现的方法或字段,这些成员必须存在于一个抽象类中,抽象类本身不能直接实例化。

抽象类的作用是作为子类的基类,内部抽象方法和属性定义,子类必须实现所有的抽象成员,规范了子类的属性注入与方法的实现。除此之外,抽象类可定义自己的方法,并让子类继承。

简单来说,抽象类是通过一个类型衍生出其子类型,让共同的方法去做同一件事情。

abstract class Duck {
abstract name: string;
abstract walk(): void;
abstract shout(): void;

getName () {
console.log(this.name);
}
}

class Bird extends Duck {
name: string;

constructor(name: string) {
super()
this.name = name
}

walk() {
console.log('Bird walk');
}

shout() {
console.log('Bird shout');
}
}

class Person extends Duck {
name: string;

constructor(name: string) {
super()
this.name = name
}

walk() {
console.log('Person walk');
}

shout() {
console.log('Person shout');
}
}

const bird: Duck = new Bird('bird')
const person: Duck = new Person('person')

bird.getName()
person.getName()

抽象成员不可被private修饰,主要针对public成员。

抽象构造函数签名

很多事情,函数可能希望接受某个类的构造函数,该构造函数可以生成从某个抽象类派生的实例。但此时TypeScript 可能会提示正在尝试实例化一个抽象类:

abstract class Base { }

function greet(ctor: typeof Base) {
// Cannot create an instance of an abstract class.
return new ctor();
}

相反,可以编写一个接受具有构造函数签名的参数的函数:

abstract class Base { }
class Derived extends Base {}

function greet(ctor: new () => Base) {
return new ctor();
}

greet(Derived);
// Argument of type 'typeof Base' is not assignable to
// parameter of type 'new () => Base'.
greet(Base);

现在,TypeScript正确地告诉您哪些类构造函数可以被调用 - Derived 可以,因为它是具体的类,但 Base 不能。

类关系

在大多数情况下,TypeScript中的类是按结构进行比较的,就像其他类型一样。考虑如下示例:

class Point1 {
x: number = 0;
y: number = 0;
}

class Point2 {
x: number = 0;
y: number = 0;
}

// It's OK
const p1: Point1 = new Point2();

同样,即使没有明确的继承关系,类之间也存在子类型关系:

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

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

const p: Person = new Employee();

但也有一些特殊情况。空类没有任何成员。在结构类型系统中,没有成员的类型通常是其他任何类型的超类型。因此,如果你编写一个空类(不要这样做!),任何类型都可以替代它使用。