Menu

TypeScript 构造函数:参数属性与重载

如何在 TypeScript 中为类的构造函数标注类型:带类型的参数和可选参数、constructor(private name: string) 这样的参数属性、字段初始化顺序、构造函数重载、子类中的 super 调用、私有构造函数,以及用 new 表示的构造函数类型。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

TypeScript 中的构造函数就是类的 constructor 方法,只是参数带有类型。它没有返回类型注解:它总是产生这个类的一个实例。

写成 constructor(): Product 会报 TS1093 错误(Type annotation cannot appear on a constructor declaration)。至于 new 的其他工作方式,都和普通 JavaScript 一样。

参数属性

上面的模式(声明字段、接收参数、把参数复制过去)太常见了,所以 TypeScript 提供了简写。在构造函数参数前加上 public、private、protected 或 readonly,它就会变成一个字段:

这是少数几个会生成代码的 TypeScript 特性之一。编译器会替你写好赋值语句:

class Product {
    name;
    price;
    sku;
    constructor(name, price, sku) {
        this.name = name;
        this.price = price;
        this.sku = sku;
    }
    // ...
}

由于它不只是会被擦除的类型,参数属性无法在 Node 内置的类型剥离下运行(node file.ts 会以 ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX 失败),erasableSyntaxOnly 编译选项也会把它报告为 TS1294 错误。直接运行 .ts 文件的项目会手动写出这些字段。

可选参数和默认参数

构造函数参数遵循和函数参数相同的规则:? 让参数变为可选,默认值让参数变为可选并自动补上值,必填参数要放在前面。

参数属性也可以有默认值。类内部 color 的类型是 string | undefined,所以使用它的代码必须先检查 undefined。

对于选项很多的构造函数,用一个选项对象比一长串按位置排列的参数更易读:constructor(opts: { prefix: string; level?: "info" | "debug" })。

字段初始化顺序

字段初始化器在构造函数体之前运行,而参数属性是在构造函数体开头才赋值的。所以字段初始化器无法读取参数属性。TypeScript 会发现这个问题:

index.ts(3,18): error TS2729: Property 'size' is used before its initialization.
index.ts(3,30): error TS2729: Property 'size' is used before its initialization.

运行时初始化器会看到 undefined,结果得到 NaN。应该改在构造函数体中计算这个值:

class Grid {
    cells: number;

    constructor(public size: number) {
        this.cells = size * size;
    }
}

console.log(new Grid(3).cells); // 9

构造函数重载

一个类只有一个构造函数实现,但你可以在它上方列出多个重载签名。调用方只能看到重载签名;实现签名必须与所有重载兼容。

超过两三种形式后,重载就变得难以阅读。名字直观的静态工厂方法(Color.fromHex("#ff8800")、Color.fromRgb(10, 20, 30))通常更清晰,也不需要在同一个函数体内收窄类型。

在子类中调用 super

声明了自己构造函数的派生类必须用父类的参数调用 super(...),而且必须在访问 this 之前调用。忘记调用会报 TS2377 错误(Constructors for derived classes must contain a 'super' call),先使用 this 则会报 TS17009('super' must be called before accessing 'this' in the constructor of a derived class)。这两条都对应着 JavaScript 的运行时规则。

class Animal {
    constructor(public name: string) {}
}

class Dog extends Animal {
    tricks: string[];

    constructor(name: string, ...tricks: string[]) {
        super(name); // must come first
        this.tricks = tricks;
    }
}

const rex = new Dog("Rex", "sit", "roll");
console.log(rex); // Dog { name: 'Rex', tricks: [ 'sit', 'roll' ] }

没有添加构造函数的子类会继承父类的构造函数,参数类型也相同。更多关于子类的内容,请参阅继承。

私有构造函数与异步初始化

private 构造函数只能在类的内部调用。在外部使用 new 会报 TS2673 错误。这样就能强制调用方通过工厂方法创建实例,适用于单例,或者必须异步完成的初始化(构造函数不能是 async 的)。

这里的 private 只是一条编译期规则。生成的 JavaScript 中是一个普通的构造函数,所以纯 JavaScript 代码仍然可以调用 new Connection(...)。

构造函数类型

要把类当作值来接收,就用构造签名为参数标注类型:new (...args) => Instance。typeof MyClass 也可以,但它会把参数绑定到这一个类的构造函数签名和静态成员上。

ConstructorParameters 和 InstanceType 可以从构造函数类型中提取参数元组和实例类型;它们和其他函数工具类型一起在 ReturnType 和 Parameters 中介绍。

常见问题

TypeScript 中的参数属性是什么?

带有修饰符(public、private、protected 或 readonly)的构造函数参数,会一步完成字段的声明和赋值。constructor(private name: string) {} 是声明 private name: string 再写 this.name = name 的简写。编译器会在输出中生成这条赋值语句。

TypeScript 类可以有多个构造函数吗?

不能有多个实现。一个类只有一个构造函数,但你可以在它上方写多个重载签名,让调用方看到不同的参数列表。Color.fromHex() 和 Color.fromRgb() 这样的静态工厂方法通常比重载更清晰。

TypeScript 中构造函数可以是 async 的吗?

不可以。构造函数总是返回新实例,而不是 promise,async constructor() 会被 TS1089 错误拒绝('async' modifier cannot appear on a constructor declaration)。改用私有构造函数加一个静态的 async create() 方法,由它完成异步工作,然后再调用 new。

如何把类的构造函数作为参数并标注类型?

使用构造签名:new (name: string) => User,或者针对这个具体的类使用 typeof User。泛型工厂函数的写法是 function make<T>(ctor: new () => T): T { return new ctor(); }。ConstructorParameters<typeof User> 会以元组形式给出参数列表。

为什么会出现 “'super' must be called before accessing 'this'” 错误?

这是 TS17009 错误。在 extends 了另一个类的类中,对象是由父类构造函数创建的,所以在 super(...) 运行之前 this 并不存在。把 super 调用移到任何访问 this 的代码之前。

Coddy 编程语言插图

和 Coddy 一起学编程

开始