TypeScript和字段初始化器

如何init一个新的类在TS以这样的方式(在c#的例子,以显示我想要的):

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

更新

写完这个答案后,更好的方法出现了。请看下面的其他答案,有更多的投票和更好的答案。我不能删除这个答案,因为它被标记为已接受。


旧的答案

TypeScript codeplex上有一个问题描述了这个:支持对象初始化器

如前所述,你已经可以通过在TypeScript中使用接口而不是类来做到这一点:

interface Name {
givenName: string;
surname: string;
}
class Person {
name: Name;
age: number;
}


var bob: Person = {
name: {
givenName: "Bob",
surname: "Smith",
},
age: 35,
};

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

您可以在类中拥有属性,然后以通常的方式分配它们。显然,他们可能需要,也可能不需要,所以这也是另一回事。只是这是一个很好的语法糖。

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

在某些情况下,使用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" }
};

您可以影响在类类型中强制转换的匿名对象。 奖励:在visual studio中,你可以这样受益于智能感知:)

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

编辑:

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

< p > 总之,应该使用接口而不是类。 最常见的用途是声明为普通旧对象的域模型。 实际上,对于域模型,您应该更好地使用接口而不是类。接口在编译时用于类型检查,与类不同,接口在编译时完全被删除
interface IModel {
Property1: string;
Property2: string;
PropertyBoolean: boolean;
PropertyNumber: number;
}


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

< >强07/12/2016更新: Typescript 2.1引入了映射类型,并提供了Partial<T>,它允许你这样做....< / p >

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语法)。我更喜欢适当的字段初始化器,但看起来还不会发生。

为了比较,如果你使用强制转换方法,你的初始化器对象必须有你要强制转换的类型的所有字段,加上不要得到任何类本身创建的类特定的函数(或派生)。

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

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

我建议一种不需要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();

重点:

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

下面是一个解决方案,它结合了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() );

如果你使用的是旧版本的typescript <2.1然后你可以使用类似于下面的,基本上是任意类型对象的类型转换:

const typedProduct = <Product>{
code: <string>product.sku
};
注意:使用此方法只适用于数据模型,因为它将删除 对象中的所有方法。它基本上是将任何对象转换为a 对象类型< / p >

我想要一个解决方案,将有以下:

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

我是这样做的:

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
  • 具有getter /setter的类

可以有一个带有可选字段(用?标记)的类和一个接收同一类实例的构造函数。

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-你必须使用类而不是接口

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

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




student: IStudentDTO = new IStudentDTO();

这是另一个解决方案:

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

初始化一个类而不重新声明默认值的所有属性:

class MyClass{
prop1!: string  //required to be passed in
prop2!: string  //required to be passed in
prop3 = 'some default'
prop4 = 123


constructor(opts:{prop1:string, prop2:string} & Partial<MyClass>){
Object.assign(this,opts)
}
}

这结合了一些已经很好的答案

这里有一个解决方案:

  • 并不强制你让所有字段都是可选的(不像Partial<...>)
  • 区分类方法和函数类型的字段(与OnlyData<...>解决方案不同)
  • 通过定义Params接口提供了一个很好的结构
  • 不需要重复变量名&类型不止一次

唯一的缺点是它一开始比较复杂。


// Define all fields here
interface PersonParams {
id: string
name?: string
coolCallback: () => string
}


// extend the params interface with an interface that has
// the same class name as the target class
// (if you omit the Params interface, you will have to redeclare
// all variables in the Person class)
interface Person extends PersonParams { }


// merge the Person interface with Person class (no need to repeat params)
// person will have all fields of PersonParams
// (yes, this is valid TS)
class Person {
constructor(params: PersonParams) {
// could also do Object.assign(this, params);


this.id = params.id;
this.name = params.name;


// intellisence will expect params
// to have `coolCallback` but not `sayHello`
this.coolCallback = params.coolCallback;
}


// compatible with functions
sayHello() {
console.log(`Hi ${this.name}!`);
}
}


// you can only export on another line (not `export default class...`)
export default Person;

对于更现代的TypeScript版本

类定义

    export class PaymentRequestDto {
public PaymentSource: number;
public PaymentCenterUid: string;
public ConnectedUserUid: string;
}

你有一些来自某处的价值观:

    const PaymentCenter= 'EA0AC01E-D34E-493B-92FF-EB2D66512345';
const PaymentSource= 4;
const ConnectedUser= '2AB0D13C-2BBE-46F5-990D-533067BE2EB3';

然后可以在使用强类型时初始化对象。

    const parameters: PaymentRequestDto = {
PaymentSource,
PaymentCenterUid: PaymentCenter,
ConnectedUserUid: ConnectedUser,
};

PaymentSource不需要名称字段说明符,因为使用的变量具有与字段相同的名称。

这也适用于数组。

    const parameters: PaymentRequestDto [] = [
{
PaymentSource,
PaymentCenterUid: PaymentCenter,
ConnectedUserUid: ConnectedUser,
},
{
. . . .
}
];

这是我找到的最好的解决办法。

声明一个可以用作装饰器的函数。我称它为AutoReflect

export function AutoReflect<T extends { new(...args: any[]): {} }>(
constructor: T
) {
return class extends constructor {
constructor(...args: any[]) {
super(args)
if (typeof args[0] === 'object') {
Object.assign(this, args[0]);
}
}
};
}
它所做的是在构造函数中期望一个对象,并将成员分配给类实例。 在类声明

上使用这个
interface IPerson {
name: string;
age: number;
}


@AutoReflect
class Person implements IPerson {
name: string;
number: number;
constructor(model?: Partial<IPerson>){}
}

在模型的构造函数中,您可以使模型成为可选的,并且在使用Partial时,您可以在不设置所有属性值的情况下新建实例

new Person({
name: 'Santa'
});

这个方法创建了一个您想要的类的新实例,并且对它有c#对象初始化的感觉。

type ExcludeMethods<T> = Pick<T, { [K in keyof T]: T[K] extends Function ? never : K }[keyof T]>;


class MyClass {
public name!: string;
public age!: number;
public optional?: boolean;
private yep: string = "";


constructor(props: ExcludeMethods<typeof MyClass.prototype>) {
Object.assign(this, props);
}


public method() {
}
}


const thing = new MyClass({
name: "bob",
age: 15
});

< a href = " https://www.typescriptlang.org/play?代码/ C4TwDgpgBAogHgYwDYFcAmECyFgAsD2aAzgDwAqAfFALxQAKAlggNbkA0UA3lANoDSUBgDsozCCHwAzKGQC6ALhn9ZUCHGAQhxKADEUQhMAb4RAfihCIANwgAnKIoEBfHmInS5FANwAoH8gBDIiIoTBAAYSQgkM4fKCgwFAAjJCYLAIBbCABCRSJgW2EAc194xJS0gKKcxSEUDKS7UoTk1IQofDAjEwCkU0Uk-HwkCAChZrBCqwCNKBAIMDyC4pooACI13zioBBN82xRDfFsACknOokV4ZHQsHAJiElBIKVCIqOCAOnPgfGeICgASi423iAHkkgArCCGT7RBhFIQnPAMIgcc5gIiA5pOPxlVppLJ4QgnYGxeK43H % 20 pbakaoorfvawadub0i0ro5pswuua0gstwbg2vqgigajabwhxobfaa”rel = " nofollownoreferrer " > TS操场< / >

这是如何……

function as_<T>(o: T) { return o; };
// ... some code before
return as_<MyClass>({ Field1 = "ASD", Field2 = "QWE" });
// ...  some code after