TypeScript和字段初始化程序


251

如何以TS这种方式初始化新类(示例C#以显示我想要的内容):

// ... some code before
return new MyClass { Field1 = "ASD", Field2 = "QWE" };
// ...  some code after

[edit]
当我写这个问题时,我是一个纯粹的.NET开发人员,没有太多的JS知识。TypeScript也是全新的东西,它是基于C#的JavaScript的超集。今天,我看到这个问题多么愚蠢。

无论如何,如果有人仍在寻找答案,请查看以下可能的解决方案。

首先要注意的是在TS中,我们不应为模型创建空类。更好的方法是创建接口或类型(取决于需求)。来自Todd Motto的好文章:https : //ultimatecourses.com/blog/classes-vs-interfaces-in-typescript

解决方案1:

type MyType = { prop1: string, prop2: string };

return <MyType> { prop1: '', prop2: '' };

解决方案2:

type MyType = { prop1: string, prop2: string };

return { prop1: '', prop2: '' } as MyType;

解决方案3(当您确实需要上课时):

class MyClass {
   constructor(public data: { prop1: string, prop2: string }) {}
}
// ...
return new MyClass({ prop1: '', prop2: '' });

要么

class MyClass {
   constructor(public prop1: string, public prop2: string) {}
}
// ...
return new MyClass('', '');

当然,在两种情况下,您可能都不需要手动转换类型,因为它们将从函数/方法返回类型中解析。


9
您刚刚附加到问题的“解决方案”无效的TypeScript或JavaScript。但是有必要指出,这是最直观的尝试。
雅各布·弗谢

1
@JacobFoshee无效的JavaScript?请参阅我的Chrome开发工具:i.imgur.com/vpathu6.png (但不可避免地会受到Visual Studio Code或任何其他受污染的TypeScript的抱怨)
Mars Robertson

5
@MichalStefanow OP在我发表评论后编辑了问题。他确实有return new MyClass { Field1: "ASD", Field2: "QWE" };
Jacob Foshee

Answers:


90

更新资料

自编写此答案以来,出现了更好的方法。请查看下面的其他投票,它们有更多的票数和更好的答案。由于此答案已被标记为接受,因此无法删除。


旧答案

TypeScript Codeplex上有一个问题对此进行了描述:支持对象初始化器

如前所述,您已经可以通过使用TypeScript中的接口代替类来做到这一点:

interface Name {
    first: string;
    last: string;
}
class Person {
    name: Name;
    age: number;
}

var bob: Person = {
    name: {
        first: "Bob",
        last: "Smith",
    },
    age: 35,
};

81
在您的示例中,bob不是Person类的实例。我看不到这等效于C#示例。
杰克·韦斯特2013年

3
界面名称最好以大写
字母

16
1. Person不是类,并且2. @JackWester是正确的,bob不是Person的实例。尝试警报(bob instanceof Person);在此代码示例中,“人员”仅用于类型声明。
雅克

15
我同意杰克和雅克的观点,我认为值得重复。你的鲍勃是的类型 Person,但它不是在所有的实例Person。想象一下,Person实际上是一个具有复杂构造函数和一堆方法的类,这种方法将面目全非。一大堆人发现您的方法很有用,但这不是解决问题的方法,因为您可以很好地在示例中使用Person类而不是接口,因为它是同一类型。
亚历克斯

26
为什么当答案明显错误时,为什么可以接受?
亨德里克·威斯

447

2016年7月12日更新:Typescript 2.1引入了Mapped Types并提供Partial<T>,可以让您执行此操作。

class Person {
    public name: string = "default"
    public address: string = "default"
    public age: number = 0;

    public constructor(init?:Partial<Person>) {
        Object.assign(this, init);
    }
}

let persons = [
    new Person(),
    new Person({}),
    new Person({name:"John"}),
    new Person({address:"Earth"}),    
    new Person({age:20, address:"Earth", name:"John"}),
];

原始答案:

我的方法是定义一个单独的fields变量,该变量传递给构造函数。技巧是重新定义此初始化程序的所有类字段为可选。创建对象(使用默认值)后,您只需将初始化程序对象分配给this

export class Person {
    public name: string = "default"
    public address: string = "default"
    public age: number = 0;

    public constructor(
        fields?: {
            name?: string,
            address?: string,
            age?: number
        }) {
        if (fields) Object.assign(this, fields);
    }
}

或手动执行(更加安全):

if (fields) {
    this.name = fields.name || this.name;       
    this.address = fields.address || this.address;        
    this.age = fields.age || this.age;        
}

用法:

let persons = [
    new Person(),
    new Person({name:"Joe"}),
    new Person({
        name:"Joe",
        address:"planet Earth"
    }),
    new Person({
        age:5,               
        address:"planet Earth",
        name:"Joe"
    }),
    new Person(new Person({name:"Joe"})) //shallow clone
]; 

和控制台输出:

Person { name: 'default', address: 'default', age: 0 }
Person { name: 'Joe', address: 'default', age: 0 }
Person { name: 'Joe', address: 'planet Earth', age: 0 }
Person { name: 'Joe', address: 'planet Earth', age: 5 }
Person { name: 'Joe', address: 'default', age: 0 }   

这为您提供了基本的安全性和属性初始化,但是所有初始化都是可选的,并且可能是乱序的。如果不传递字段,则将剩下班级的默认值。

您也可以将其与必需的构造函数参数混合使用-坚持fields到底。

我认为,这与C#风格差不多(实际的field-init语法被拒绝)。我非常希望使用适当的字段初始化程序,但看起来还不会发生。

为了进行比较,如果使用强制转换方法,则初始化程序对象必须具有要转换为的类型的所有字段,并且不要获取由类本身创建的任何特定于类的函数(或派生类)。


1
+1。实际上,这会创建一个类的实例(大多数解决方案都没有),将所有功能保留在构造函数中(没有Object.create或其他类),并显式声明属性名称,而不是依赖于参数排序(我的个人喜好)。但是,它确实失去了参数和属性之间的类型/编译安全性。
Fiddles '16

1
@ user1817787,最好在类本身中将具有默认值的任何内容定义为可选内容,但最好分配一个默认值。然后,不要使用Partial<>,而只是Person-这将要求您传递具有必填字段的对象。也就是说,请参阅此处以了解将类型映射限制为某些字段的想法(请参阅选择)。
Meirion Hughes

2
这确实应该是答案。这是解决此问题的最佳方法。
Ascherer '17

9
那是public constructor(init?:Partial<Person>) { Object.assign(this, init); }
金色

3
如果Object.assign显示的错误类似于我的错误,请参阅以下SO回答:stackoverflow.com/a/38860354/2621693
Jim Yarbro,

27

下面是一个解决方案,结合了较短的应用程序Object.assign以更紧密地建模原始C#模式。

但是首先,让我们回顾一下到目前为止提供的技术,其中包括:

  1. 复制接受对象的构造函数并将其应用于 Object.assign
  2. Partial<T>复制构造函数中的一个巧妙技巧
  3. 对POJO使用“广播”
  4. 利用Object.create而不是Object.assign

当然,每个人都有其优点/缺点。修改目标类以创建复制构造函数可能并不总是一种选择。并且“广播”失去了与目标类型相关的任何功能。 Object.create似乎不太吸引人,因为它需要相当冗长的属性描述符映射。

最短的通用答案

因此,这是另一种稍微简单一些的方法,它可以维护类型定义和关联的函数原型,并可以更紧密地建模所需的C#模式:

const john = Object.assign( new Person(), {
    name: "John",
    age: 29,
    address: "Earth"
});

而已。C#模式上唯一的加法是Object.assign加上2个括号和一个逗号。查看下面的工作示例,以确认它维护该类型的函数原型。不需要构造函数,也不需要聪明的技巧。

工作实例

本示例说明如何使用C#字段初始化程序的近似值来初始化对象:

class Person {
    name: string = '';
    address: string = '';
    age: number = 0;

    aboutMe() {
        return `Hi, I'm ${this.name}, aged ${this.age} and from ${this.address}`;
    }
}

// typescript field initializer (maintains "type" definition)
const john = Object.assign( new Person(), {
    name: "John",
    age: 29,
    address: "Earth"
});

// initialized object maintains aboutMe() function prototype
console.log( john.aboutMe() );


也可以像这样,通过javascript对象创建用户对象
Dave Keane

1
6½年后,我忘记了这一点,但我仍然喜欢它。再次感谢。
PRMan

25

您可以影响以您的类类型转换的匿名对象。 奖励:在Visual Studio中,您可以通过以下方式从智能感知中受益:)

var anInstance: AClass = <AClass> {
    Property1: "Value",
    Property2: "Value",
    PropertyBoolean: true,
    PropertyNumber: 1
};

编辑:

警告如果类具有方法,则您的类的实例将无法获取它们。如果AClass具有构造函数,则不会执行该构造函数。如果使用instanceof AClass,则将得到false。

总之,您应该使用interface而不是class。最常见的用途是声明为“普通旧对象”的域模型。实际上,对于域模型,您最好使用接口而不是类。接口在编译时用于类型检查,与类不同,接口在编译过程中被完全删除。

interface IModel {
   Property1: string;
   Property2: string;
   PropertyBoolean: boolean;
   PropertyNumber: number;
}

var anObject: IModel = {
     Property1: "Value",
     Property2: "Value",
     PropertyBoolean: true,
     PropertyNumber: 1
 };

20
如果AClass包含方法,anInstance就不会得到它们。
Monsignor

2
同样,如果AClass具有构造函数,则不会执行该构造函数。
迈克尔·埃里克森

1
另外,如果您这样做anInstance instanceof AClass,则会false在运行时获得。
Lucero

15

我建议一种不需要Typescript 2.1的方法:

class Person {
    public name: string;
    public address?: string;
    public age: number;

    public constructor(init:Person) {
        Object.assign(this, init);
    }

    public someFunc() {
        // todo
    }
}

let person = new Person(<Person>{ age:20, name:"John" });
person.someFunc();

关键点:

  • 打字稿2.1不需要,Partial<T>不需要
  • 它支持函数(与不支持函数的简单类型断言相比)

1
它不遵守必填字段:(new Person(<Person>{});因为强制转换),并且也要明确;使用Partial <T>支持功能。最终,如果您具有必填字段(加上原型功能),则需要执行以下操作:init: { name: string, address?: string, age: number }并删除演员表。
Meirion Hughes

同样,当我们获得条件类型映射时,您将能够仅将函数映射到partials,并按原样保留属性。:)
Meirion Hughes

取而代之的是复杂的class,然后var,如果我这样做var dog: {name: string} = {name: 'will be assigned later'};,它编译和作品。有任何不足或问题吗?哦,dog范围和特定范围非常小,仅意味着一个实例。
Jeb50 '19

11

我会更倾向于使用这种方式,使用(可选)自动属性和默认值。您没有建议这两个字段是数据结构的一部分,所以这就是我选择这种方式的原因。

您可以在类上具有属性,然后按常规方式分配它们。显然,它们可能是必需的,也可能不是必需的,所以这也是另一回事。只是这是一个很好的语法糖。

class MyClass{
    constructor(public Field1:string = "", public Field2:string = "")
    {
        // other constructor stuff
    }
}

var myClass = new MyClass("ASD", "QWE");
alert(myClass.Field1); // voila! statement completion on these properties

6
我最深的歉意。但是您实际上并没有“询问字段初始化器”,因此很自然地假设您可能对在TS中更新类的替代方式感兴趣。如果您已经准备好投票,那么您可能会在问题中提供更多的信息。
拉尔夫·拉维尔

+1构造函数是可行的方法;但是,如果您有很多字段并且只想初始化其中一些字段,我想我的回答将使事情变得更容易。
Meirion Hughes

1
如果您有很多字段,那么像这样初始化一个对象将非常麻烦,因为您将向构造函数传递一面无名的值。但这并不是说这种方法没有优点。最好将其用于具有很少字段的简单对象。大多数出版物说,成员签名中的大约四个或五个参数是上限。只是指出这一点是因为我在搜索TS初始化程序时在某人的博客上找到了此解决方案的链接。我有20多个字段的对象,需要针对单元测试进行初始化。
达斯汀·克利夫兰

11

在某些情况下,可以接受使用Object.create。如果您需要向后兼容或想滚动自己的初始化函数,则Mozilla参考资料包括一个polyfill。

应用于您的示例:

Object.create(Person.prototype, {
    'Field1': { value: 'ASD' },
    'Field2': { value: 'QWE' }
});

有用的场景

  • 单元测试
  • 内联声明

就我而言,我发现它在单元测试中很有用,原因有两个:

  1. 测试期望时,我经常想创建一个苗条的对象作为期望
  2. 单元测试框架(例如Jasmine)可能会比较对象原型(__proto__)并导致测试失败。例如:
var actual = new MyClass();
actual.field1 = "ASD";
expect({ field1: "ASD" }).toEqual(actual); // fails

单元测试失败的输出不会提供有关不匹配项的线索。

  1. 在单元测试中,我可以选择支持哪些浏览器

最后,在http://typescript.codeplex.com/workitem/334上提出的解决方案不支持内联json样式声明。例如,以下内容不会编译:

var o = { 
  m: MyClass: { Field1:"ASD" }
};

9

我想要一个具有以下优点的解决方案:

  • 所有数据对象都是必需的,并且必须由构造函数填充。
  • 无需提供默认值。
  • 可以在类内部使用函数。

这是我的操作方式:

export class Person {
  id!: number;
  firstName!: string;
  lastName!: string;

  getFullName() {
    return `${this.firstName} ${this.lastName}`;
  }

  constructor(data: OnlyData<Person>) {
    Object.assign(this, data);
  }
}

const person = new Person({ id: 5, firstName: "John", lastName: "Doe" });
person.getFullName();

构造函数中的所有属性都是强制性的,如果没有编译器错误,则不得省略。

它依赖于OnlyData过滤掉getFullName()所需属性的属性,其定义如下:

// based on : https://medium.com/dailyjs/typescript-create-a-condition-based-subset-types-9d902cea5b8c
type FilterFlags<Base, Condition> = { [Key in keyof Base]: Base[Key] extends Condition ? never : Key };
type AllowedNames<Base, Condition> = FilterFlags<Base, Condition>[keyof Base];
type SubType<Base, Condition> = Pick<Base, AllowedNames<Base, Condition>>;
type OnlyData<T> = SubType<T, (_: any) => any>;

这种方式的当前局限性:

  • 需要TypeScript 2.8
  • 带吸气剂/吸气剂的课程

这个答案对我来说似乎最接近理想。想知道我们能否用打字稿3+做得更好
RyanM

1
据我所知,这是迄今为止最好的方法。谢谢。
弗洛里安·诺伯特·本彭克

此解决方案有一个问题,@VitalyB:一旦方法具有参数,就会中断:getFullName(){return“ bar”}起作用时,getFullName(str:string):string {return str}不起作用
Florian Norbert bepunkt

@floriannorbertbepunkt到底什么对您不起作用?它对我来说似乎很好...
rfgamaral

3

这是另一个解决方案:

return {
  Field1 : "ASD",
  Field2 : "QWE" 
} as myClass;

2

您可能有一个带有可选字段的类(标记为?),并且有一个接收相同类实例的构造函数。

class Person {
    name: string;     // required
    address?: string; // optional
    age?: number;     // optional

    constructor(person: Person) {
        Object.assign(this, person);
    }
}

let persons = [
    new Person({ name: "John" }),
    new Person({ address: "Earth" }),    
    new Person({ age: 20, address: "Earth", name: "John" }),
];

在这种情况下,您将无法省略必填字段。这使您可以对对象构造进行细粒度的控制。

您可以将构造函数与Partial类型一起使用,如其他答案所述:

public constructor(init?:Partial<Person>) {
    Object.assign(this, init);
}

问题在于所有字段都变为可选字段,在大多数情况下不希望这样做。


1

最简单的方法是使用类型转换。

return <MyClass>{ Field1: "ASD", Field2: "QWE" };

7
不幸的是,(1)这不是类型转换,而是编译时类型断言,(2)这个问题问“如何初始化一个新(强调我的),这种方法将无法实现。如果TypeScript具有此功能肯定会很好,但是不幸的是,事实并非如此。
John Weisz

的定义在MyClass哪里?
Jeb50 '19

0

如果您使用的旧版打字稿<2.1,则可以使用类似于以下内容的方法,该方法基本上是将任何类型的对象强制转换为:

const typedProduct = <Product>{
                    code: <string>product.sku
                };

注意:使用此方法仅对数据模型有用,因为它将删除对象中的所有方法。基本上是将任何对象强制转换为类型化对象


-1

如果要创建新实例而不设置实例初始值

1-您必须使用类而不是接口

2-创建类时必须设置初始值

export class IStudentDTO {
 Id:        number = 0;
 Name:      string = '';


student: IStudentDTO = new IStudentDTO();
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.