TypeScript中的构造函数重载

有人在TypeScript中做过构造函数重载吗?在语言规范(v 0.8)的第64页,有描述构造函数重载的语句,但没有给出任何示例代码。

我现在正在尝试一个非常基本的类声明;它是这样的,

interface IBox {
x : number;
y : number;
height : number;
width : number;
}


class Box {
public x: number;
public y: number;
public height: number;
public width: number;


constructor(obj: IBox) {
this.x = obj.x;
this.y = obj.y;
this.height = obj.height;
this.width = obj.width;
}


constructor() {
this.x = 0;
this.y = 0;
this.width = 0;
this.height = 0;
}
}

当运行tsc BoxSample。Ts,它抛出一个重复的构造函数定义——这是显而易见的。任何帮助都是感激的。

382093 次浏览

TypeScript允许你声明重载,但是你只能有一个实现,而且这个实现必须有一个与所有重载兼容的签名。在你的例子中,这可以很容易地用一个可选参数来完成,

interface IBox {
x : number;
y : number;
height : number;
width : number;
}
    

class Box {
public x: number;
public y: number;
public height: number;
public width: number;


constructor(obj?: IBox) {
this.x = obj?.x ?? 0
this.y = obj?.y ?? 0
this.height = obj?.height ?? 0
this.width = obj?.width ?? 0;
}
}

或者使用更通用的构造函数进行两次重载,

interface IBox {
x : number;
y : number;
height : number;
width : number;
}
    

class Box {
public x: number;
public y: number;
public height: number;
public width: number;


constructor();
constructor(obj: IBox);
constructor(obj?: IBox) {
this.x = obj?.x ?? 0
this.y = obj?.y ?? 0
this.height = obj?.height ?? 0
this.width = obj?.width ?? 0;
}
}

操场上

注意,你也可以通过TypeScript中的默认参数来解决在实现级别上缺乏重载的问题,例如:

interface IBox {
x : number;
y : number;
height : number;
width : number;
}


class Box {
public x: number;
public y: number;
public height: number;
public width: number;


constructor(obj : IBox = {x:0,y:0, height:0, width:0}) {
this.x = obj.x;
this.y = obj.y;
this.height = obj.height;
this.width = obj.width;
}
}

< em >编辑: 截至16年12月5日,请参阅本森的回答,以获得更详细的解决方案,允许更大的灵活性

这种语言在不断发展,所以如果您可以使用Partial(在2.1版中引入),那么这是我现在最喜欢的实现方法。

class Box {
x: number;
y: number;
height: number;
width: number;


public constructor(b: Partial<Box> = {}) {
Object.assign(this, b);
}
}


// Example use
const a = new Box();
const b = new Box({x: 10, height: 99});
const c = new Box({foo: 10});          // Will fail to compile

更新(2017年6月8日): guyarad和snolflake在下面的评论中对我的回答提出了有效的观点。我建议读者看看本森snolflake的答案,他们的答案比我的更好

原答案(2014年1月27日)

另一个如何实现构造函数重载的例子:

class DateHour {


private date: Date;
private relativeHour: number;


constructor(year: number, month: number, day: number, relativeHour: number);
constructor(date: Date, relativeHour: number);
constructor(dateOrYear: any, monthOrRelativeHour: number, day?: number, relativeHour?: number) {
if (typeof dateOrYear === "number") {
this.date = new Date(dateOrYear, monthOrRelativeHour, day);
this.relativeHour = relativeHour;
} else {
var date = <Date> dateOrYear;
this.date = new Date(date.getFullYear(), date.getMonth(), date.getDate());
this.relativeHour = monthOrRelativeHour;
}
}
}

来源:# EYZ0

我知道这是一个老问题,但1.4中的新功能是联合类型;对所有函数重载(包括构造函数)使用这些。例子:

class foo {
private _name: any;
constructor(name: string | number) {
this._name = name;
}
}
var f1 = new foo("bar");
var f2 = new foo(1);

关于构造函数的重载,一个好的替代方案是将额外的重载实现为静态工厂方法。我认为它比在构造函数中检查所有可能的参数组合更易于阅读和更容易。

在下面的例子中,我们能够使用来自保险提供商的数据创建一个患者对象,这些数据存储的值不同。为了支持患者实例化的另一种数据结构,可以简单地添加另一个静态方法,以便在规范化所提供的数据后尽可能地调用默认构造函数。

class Patient {
static fromInsurance({
first, middle = '', last,
birthday, gender
}: InsuranceCustomer): Patient {
return new this(
`${last}, ${first} ${middle}`.trim(),
utils.age(birthday),
gender
);
}


constructor(
public name: string,
public age: number,
public gender?: string
) {}
}


interface InsuranceCustomer {
first: string,
middle?: string,
last: string,
birthday: string,
gender: 'M' | 'F'
}




const utils = { /* included in the playground link below */};


{// Two ways of creating a Patient instance
const
jane = new Patient('Doe, Jane', 21),
alsoJane = Patient.fromInsurance({
first: 'Jane', last: 'Doe',
birthday: 'Jan 1, 2000', gender: 'F'
})


console.clear()
console.log(jane)
console.log(alsoJane)
}


您可以在TS操场处检查输出


比方说,TypeScript 不是真的中的方法重载,因为它需要太多编译器生成的代码,而TS的设计是不惜一切代价避免这种情况。方法重载的主要用例可能是为API中有神奇参数的库编写声明。由于处理不同可能的参数集的所有繁重工作都是由您完成的,因此我不认为在每种场景中使用重载而不是特别方法有多大优势。

在可选的类型参数足够好的情况下,考虑以下代码,它可以在不重复属性或定义接口的情况下实现相同的功能:

export class Track {
public title: string;
public artist: string;
public lyrics: string;


constructor(track?: Track) {
Object.assign(this, track);
}
}

请记住,这将分配在track中传递的所有属性,如果它们没有在Track中定义。

interface IBox {
x: number;
y: number;
height: number;
width: number;
}


class Box {
public x: number;
public y: number;
public height: number;
public width: number;


constructor(obj: IBox) {
const { x, y, height, width } = { x: 0, y: 0, height: 0, width: 0, ...obj }
this.x = x;
this.y = y;
this.height = height;
this.width = width;
}
}

听起来像是希望对象参数是可选的,对象中的每个属性也是可选的。在本例中,不需要重载语法。我想在这里指出一些答案中的一些错误做法。当然,它并不是编写box = { x: 0, y: 87, width: 4, height: 0 }的最小表达式,但是它提供了您可能希望从类中得到的所有代码提示。这个例子允许你用一个,一些,所有,没有任何参数来调用一个函数,并且仍然得到默认值。

 /** @class */
class Box {
public x?: number;
public y?: number;
public height?: number;
public width?: number;


constructor(params: Box = {} as Box) {


// Define the properties of the incoming `params` object here.
// Setting a default value with the `= 0` syntax is optional for each parameter
let {
x = 0,
y = 0,
height = 1,
width = 1
} = params;
         

//  If needed, make the parameters publicly accessible
//  on the class ex.: 'this.var = var'.
/**  Use jsdoc comments here for inline ide auto-documentation */
this.x = x;
this.y = y;
this.height = height;
this.width = width;
}
}

一个详细但更可扩展的替代方案: 上面的Box类可以作为接口双重工作,因为它们是相同的。如果选择修改上面的类,则需要为传入参数对象定义和引用一个新接口,因为Box类将不再完全类似于传入参数。注意在这种情况下,表示可选属性的问号(?:)移动到哪里。因为我们在类中设置了默认值,所以它们保证会出现,但是它们在传入参数对象中是可选的:

    interface BoxParams {
x?: number;
// Add Parameters ...
}


class Box {
public x: number;
// Copy Parameters ...
constructor(params: BoxParams = {} as BoxParams) {
let { x = 0 } = params;
this.x = x;
}
doSomething = () => {
return this.x + this.x;
}
}


无论你选择哪种方式来定义你的类,这种技术都提供了类型安全的护栏,同时还可以灵活地编写以下任何一种:

const box1 = new Box();
const box2 = new Box({});
const box3 = new Box({x:0});
const box4 = new Box({x:0, height:10});
const box5 = new Box({x:0, y:87,width:4,height:0});


// Correctly reports error in TypeScript, and in js, box6.z is undefined
const box6 = new Box({z:0});

编译后,您将看到如何仅在可选值未定义时使用默认设置;它通过检查void 0 (undefined的简写)来避免广泛使用(但容易出错)的var = isOptional || default;回退语法的陷阱:

编译后的输出

var Box = (function () {
function Box(params) {
if (params === void 0) { params = {}; }
var _a = params.x, x = _a === void 0 ? 0 : _a, _b = params.y, y = _b === void 0 ? 0 : _b, _c = params.height, height = _c === void 0 ? 1 : _c, _d = params.width, width = _d === void 0 ? 1 : _d;
this.x = x;
this.y = y;
this.height = height;
this.width = width;
}
return Box;
}());

附录:设置默认值:错误的方式

||(或)操作符

在设置默认回退值时,考虑||/or操作符的危险,如其他一些答案所示。下面的代码说明了设置默认值的错误方法。当你对美甲师值进行计算时,你可能会得到意想不到的结果,比如0,",null, undefined, false, NaN:

var myDesiredValue = 0;
var result = myDesiredValue || 2;


// This test will correctly report a problem with this setup.
console.assert(myDesiredValue === result && result === 0, 'Result should equal myDesiredValue. ' + myDesiredValue + ' does not equal ' + result);

Object.assign (params)

在我的测试中,使用es6/typescript解构对象可以比Object.assign快15-90%。使用解构形参只允许您分配给对象的方法和属性。例如,考虑这个方法:

class BoxTest {
public x?: number = 1;


constructor(params: BoxTest = {} as BoxTest) {
Object.assign(this, params);
}
}

如果另一个用户没有使用TypeScript,并且试图放置一个不属于TypeScript的参数,比如说,他们可能会尝试放置一个z属性

var box = new BoxTest({x: 0, y: 87, width: 4, height: 0, z: 7});


// This test will correctly report an error with this setup. `z` was defined even though `z` is not an allowed property of params.
console.assert(typeof box.z === 'undefined')

你应该记住……

contructor()


constructor(a:any, b:any, c:any)

它与new()new("a","b","c")相同

因此

constructor(a?:any, b?:any, c?:any)

和上面一样,而且更灵活…

new()new("a")new("a","b")new("a","b","c")

你可以通过以下方法来处理:

class Box {
x: number;
y: number;
height: number;
width: number;
constructor(obj?: Partial<Box>) {
assign(this, obj);
}
}

Partial将使你的字段(x,y,高度,宽度)可选,允许多个构造函数

你可以做new Box({x,y})没有高度和宽度。

我们可以使用警卫来模拟构造函数重载

interface IUser {
name: string;
lastName: string;
}


interface IUserRaw {
UserName: string;
UserLastName: string;
}


function isUserRaw(user): user is IUserRaw {
return !!(user.UserName && user.UserLastName);
}


class User {
name: string;
lastName: string;


constructor(data: IUser | IUserRaw) {
if (isUserRaw(data)) {
this.name = data.UserName;
this.lastName = data.UserLastName;
} else {
this.name = data.name;
this.lastName = data.lastName;
}
}
}


const user  = new User({ name: "Jhon", lastName: "Doe" })
const user2 = new User({ UserName: "Jhon", UserLastName: "Doe" })

实际上,现在回答这个问题可能太晚了,但你现在可以这样做:

class Box {
public x: number;
public y: number;
public height: number;
public width: number;


constructor();
constructor(obj: IBox);
constructor(obj?: IBox) {
this.x = !obj ? 0 : obj.x;
this.y = !obj ? 0 : obj.y;
this.height = !obj ? 0 : obj.height;
this.width = !obj ? 0 : obj.width;
}
}

因此,你可以不使用静态方法,而是使用上述方法。希望能对你有所帮助!!

我使用以下替代方法来获得默认/可选参数和“kind-of-overloaded”的构造函数,参数数量可变:

private x?: number;
private y?: number;


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

我知道这不是最漂亮的代码,但人们会习惯它。不需要额外的接口,它允许私有成员,这在使用接口时是不可能的。

您的Box类试图定义多个构造函数实现

只有最后一个构造函数过载签名被用作类构造函数实现

在下面的示例中,请注意,构造函数实现的定义使与前面的过载签名中的任何一个相矛盾。

interface IBox = {
x: number;
y: number;
width: number;
height: number;
}


class Box {
public x: number;
public y: number;
public width: number;
public height: number;


constructor() /* Overload Signature */
constructor(obj: IBox) /* Overload Signature */
constructor(obj?: IBox) /* Implementation Constructor */ {
if (obj) {
this.x = obj.x;
this.y = obj.y;
this.width = obj.width;
this.height = obj.height;
} else {
this.x = 0;
this.y = 0;
this.width = 0;
this.height = 0
}
}


get frame(): string {
console.log(this.x, this.y, this.width, this.height);
}
}


new Box().frame; // 0 0 0 0
new Box({ x:10, y:10, width: 70, height: 120 }).frame; // 10 10 70 120






// You could also write the Box class like so;
class Box {
public x: number = 0;
public y: number = 0;
public width: number = 0;
public height: number = 0;


constructor() /* Overload Signature */
constructor(obj: IBox) /* Overload Signature */
constructor(obj?: IBox) /* Implementation Constructor */ {
if (obj) {
this.x = obj.x;
this.y = obj.y;
this.width = obj.width;
this.height = obj.height;
}
}


get frame(): string { ... }
}

下面是一个工作示例,您必须考虑每个具有更多字段的构造函数都应该将额外的字段标记为optional

class LocalError {
message?: string;
status?: string;
details?: Map<string, string>;


constructor(message: string);
constructor(message?: string, status?: string);
constructor(message?: string, status?: string, details?: Map<string, string>) {
this.message = message;
this.status = status;
this.details = details;
}
}

正如@Benson回答中所评论的,我在我的代码中使用了这个例子,我发现它非常有用。然而,当我试图用我的类变量类型进行计算时,我发现了Object is possibly 'undefined'.ts(2532)错误,因为问号导致它们的类型为AssignedType | undefined。即使未定义的情况是处理在以后的执行或与编译器类型强制<AssignedType>我无法摆脱错误,所以不能使参数可选。我解决了为带问号参数的参数和不带问号的类变量创建一个分开的类型。啰嗦,但很管用。

下面是原始代码,给出了类方法()中的错误,如下所示:

/** @class */


class Box {
public x?: number;
public y?: number;
public height?: number;
public width?: number;


// The Box class can work double-duty as the interface here since they are identical
// If you choose to add methods or modify this class, you will need to
// define and reference a new interface for the incoming parameters object
// e.g.:  `constructor(params: BoxObjI = {} as BoxObjI)`
constructor(params: Box = {} as Box) {
// Define the properties of the incoming `params` object here.
// Setting a default value with the `= 0` syntax is optional for each parameter
const {
x = 0,
y = 0,
height = 1,
width = 1,
} = params;


//  If needed, make the parameters publicly accessible
//  on the class ex.: 'this.var = var'.
/**  Use jsdoc comments here for inline ide auto-documentation */
this.x = x;
this.y = y;
this.height = height;
this.width = width;
}


method(): void {
const total = this.x + 1; // ERROR. Object is possibly 'undefined'.ts(2532)
}
}


const box1 = new Box();
const box2 = new Box({});
const box3 = new Box({ x: 0 });
const box4 = new Box({ x: 0, height: 10 });
const box5 = new Box({ x: 0, y: 87, width: 4, height: 0 });

变量不能在类方法中使用。 如果像这样纠正,例如:

method(): void {
const total = <number> this.x + 1;
}

现在出现这个错误:

Argument of type '{ x: number; y: number; width: number; height: number; }' is not
assignable to parameter of type 'Box'.
Property 'method' is missing in type '{ x: number; y: number; width: number; height:
number; }' but required in type 'Box'.ts(2345)

好像整个arguments bundle不再是可选的了。

因此,如果创建了带有可选参数的类型,并且从可选参数中删除了类变量,我就实现了我想要的,参数是可选的,并且能够在类方法中使用它们。下面是解决方案代码:

type BoxParams = {
x?: number;
y?: number;
height?: number;
width?: number;
}


/** @class */
class Box {
public x: number;
public y: number;
public height: number;
public width: number;


// The Box class can work double-duty as the interface here since they are identical
// If you choose to add methods or modify this class, you will need to
// define and reference a new interface for the incoming parameters object
// e.g.:  `constructor(params: BoxObjI = {} as BoxObjI)`
constructor(params: BoxParams = {} as BoxParams) {
// Define the properties of the incoming `params` object here.
// Setting a default value with the `= 0` syntax is optional for each parameter
const {
x = 0,
y = 0,
height = 1,
width = 1,
} = params;


//  If needed, make the parameters publicly accessible
//  on the class ex.: 'this.var = var'.
/**  Use jsdoc comments here for inline ide auto-documentation */
this.x = x;
this.y = y;
this.height = height;
this.width = width;
}


method(): void {
const total = this.x + 1;
}
}


const box1 = new Box();
const box2 = new Box({});
const box3 = new Box({ x: 0 });
const box4 = new Box({ x: 0, height: 10 });
const box5 = new Box({ x: 0, y: 87, width: 4, height: 0 });

感谢任何花时间阅读并试图理解我想要表达的观点的人的评论。

提前谢谢你。

一般来说,对于N个重载,最好使用:

constructor(obj?: {fromType1: IType1} | {fromType2: IType2}) {
if(obj){
if(obj.fromType1){
//must be of form IType1
} else if(obj.fromType2){
//must have used a IType2
} else {
throw "Invalid argument 1"
}
} else {
//obj not given
}
}

至少现在我们可以确定走哪条路,并采取相应的行动

正如chuckj所说,简单的答案是一个可选形参,但是如果我们想用多个形参重载构造函数,或者我们想改变形参顺序,该怎么办呢?

事实证明,构造函数可以像函数一样重载:

class FooBar {
public foo?: number;
public bar?: string;


// Constructor A
constructor(foo: number, bar?: string);
// Constructor B
constructor(bar: string, foo?: number);
// Constructor C
constructor(bar: string);
// Constructor D
constructor(foo: number);
// Constructor E
constructor();


constructor(...args: any[]) {
switch (args.length) {
case 2:
if (typeof args[0] === "number") {
this.foo = args[0];
this.bar = args[1];
} else {
this.bar = args[0];
this.foo = args[1];
}
break;
case 1:
if (typeof args[0] === "number") {
this.foo = args[0];
} else {
this.bar = args[0];
}
}


console.log(this.foo, this.bar);
}
}


const fooBarConstructorA = new FooBar("150", 25);
const fooBarConstructorB = new FooBar(25, "150");
const fooBarConstructorC = new FooBar("150");
const fooBarConstructorD = new FooBar("150");
const fooBarConstructorE = new FooBar();