TypeScript和字段初始值设定项

Nic*_*kon 196 typescript

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

// ... some code before
return new MyClass { Field1 = "ASD", Field2 = "QWE" };
// ...  some code after
Run Code Online (Sandbox Code Playgroud)

解决方案:
经典TS语法:

// ... some code before
return new MyClass { Field1 = "ASD", Field2 = "QWE" };
// ...  some code after
Run Code Online (Sandbox Code Playgroud)

Mei*_*hes 384

更新时间07/12/2016: 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"}),
];
Run Code Online (Sandbox Code Playgroud)

原答案:

我的方法是定义一个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);
    }
}
Run Code Online (Sandbox Code Playgroud)

或者手动完成(更安全一点):

if (fields) {
    this.name = fields.name || this.name;       
    this.address = fields.address || this.address;        
    this.age = fields.age || this.age;        
}
Run Code Online (Sandbox Code Playgroud)

用法:

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
]; 
Run Code Online (Sandbox Code Playgroud)

和控制台输出:

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 }   
Run Code Online (Sandbox Code Playgroud)

这为您提供了基本的安全性和属性初始化,但它全部是可选的,可以是无序的.如果不传递字段,则单独保留类的默认值.

您也可以将它与所需的构造函数参数混合 - 坚持fields到最后.

关于接近C#样式我认为(实际的field-init语法被拒绝).我更喜欢适当的字段初始化,但看起来还不会发生.

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

  • 那是GOLDEN的一段代码`public constructor(init ?: Partial <Person>){Object.assign(this,init); }` (7认同)
  • +1。这实际上创建了一个类的实例(大多数解决方案都没有),将所有功能保留在构造函数内(没有 Object.create 或其他多余的东西),并显式声明属性名称而不是依赖于参数排序(这是我个人的偏好)。不过,它确实失去了参数和属性之间的类型/编译安全性。 (2认同)
  • @ user1817787 您最好在类本身中将任何具有默认值的内容定义为可选,但分配一个默认值。然后不要使用`Partial&lt;&gt;`,只使用`Person` - 这将要求您传递一个具有所需字段的对象。也就是说,[参见此处](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html) 了解将类型映射限制到某些字段的想法(参见 Pick)。 (2认同)
  • 这确实应该是答案。它是此问题的最佳解决方案。 (2认同)
  • 如果Object.assign显示的错误类似于我的错误,请参见以下答案:/sf/answers/2720224811/ (2认同)
  • 如果类中有非空引用属性,则它不起作用。TSC 无法将 `Object.assign` 识别为属性初始值设定项,因此您会收到此错误:“属性 'x' 没有初始值设定项,并且未在构造函数中明确分配。” (2认同)

Wou*_*ort 73

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,
};
Run Code Online (Sandbox Code Playgroud)

  • 在您的示例中,bob不是Person类的实例.我不知道这与C#示例相同. (76认同)
  • 当明显错误时,为什么这是一个公认的答案? (24认同)
  • 1.人不是一个班级而且2. @JackWester是对的,bob不是一个人的实例.尝试提醒(bob instanceof Person); 在此代码示例中,Person仅用于Type Assertion目的. (16认同)
  • 我同意杰克和杰克斯,我认为值得重复.你的bob属于*type*`Person`,但它根本不是`Person`的实例.想象一下,"Person"实际上是一个具有复杂构造函数和一堆方法的类,这种方法会在它的表面上掉线.很多人发现你的方法很有用,但它不是问题的解决方案,因为它已经说明了,你也可以在你的例子中使用类Person而不是接口,它将是相同的类型. (14认同)
  • 界面名称最好以大写"I"开头 (3认同)
  • 只要答案被接受,我就无法删除它。 (2认同)
  • 也许添加一个更好的方法(如下所示)的注释可能会有所帮助? (2认同)

Jon*_* B. 21

下面是一个解决方案,它结合了更短的应用程序,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"
});
Run Code Online (Sandbox Code Playgroud)

而已.C#模式上唯一的补充是Object.assign2个括号和逗号.查看下面的工作示例,确认它维护了类型的函数原型.不需要构造函数,也没有聪明的技巧.

工作实例

此示例显示如何使用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() );
Run Code Online (Sandbox Code Playgroud)


rdh*_*aut 20

您可以影响在类类型中输入的匿名对象. 额外奖励:在视觉工作室中,你可以通过这种方式受益于intellisense :)

var anInstance: AClass = <AClass> {
    Property1: "Value",
    Property2: "Value",
    PropertyBoolean: true,
    PropertyNumber: 1
};
Run Code Online (Sandbox Code Playgroud)

编辑:

警告如果类有方法,则类的实例将不会获取它们,并且不会执行构造函数.

此解决方案应仅用于接口.例如,使用此解决方案将模型存储为Plain Old Object.

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

var anObject: IModel = {
     Property1: "Value",
     Property2: "Value",
     PropertyBoolean: true,
     PropertyNumber: 1
 };
Run Code Online (Sandbox Code Playgroud)

  • 如果`AClass`包含方法,`anInstance`将无法获取它们. (19认同)
  • 此外,如果AClass有一个构造函数,它将不会被执行. (2认同)
  • 此外,如果你执行 `anInstance instanceof AClass`,你会在运行时得到 `false`。 (2认同)

Veg*_*ter 13

我建议一种不需要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();
Run Code Online (Sandbox Code Playgroud)

关键点:

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

  • 它不尊重必填字段:`new Person(&lt;Person&gt;{});`(因为转换)并且也很清楚;使用 Partial&lt;T&gt; 支持函数。最终,如果您有必填字段(加上原型函数),您需要执行以下操作:`init: { name: string, address?: string, age: number }` 并删除强制转换。 (3认同)

小智 13

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

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" }),
];
Run Code Online (Sandbox Code Playgroud)

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

您可以使用具有 Partial 类型的构造函数,如其他答案中所述:

public constructor(init?:Partial<Person>) {
    Object.assign(this, init);
}
Run Code Online (Sandbox Code Playgroud)

问题是所有字段都变成可选的,并且在大多数情况下是不可取的。

  • 这个答案的问题是,如果 Person 类有方法,那么构造函数的参数也应该有方法。 (4认同)

Vit*_*lyB 12

我想要一个具有以下内容的解决方案:

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

这是我这样做的方式:

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();
Run Code Online (Sandbox Code Playgroud)

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

它取决于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>;
Run Code Online (Sandbox Code Playgroud)

这种方式的当前限制:

  • 需要打字稿 2.8
  • 带有 getter/setter 的类

  • 据我所知,这是迄今为止最好的前进方式。谢谢。 (2认同)
  • 这个解决方案有一个问题,@VitalyB:一旦方法有参数,就会中断:虽然 getFullName() { return "bar" } 有效,但 getFullName(str: string):string { return str } 不起作用 (2认同)

Ral*_*lle 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
Run Code Online (Sandbox Code Playgroud)

  • 我最深切的道歉.但是你实际上并没有"询问字段初始化器",所以很自然地假设你可能对在TS中新建一个类的替代方法感兴趣.如果您准备好进行投票,您可以在问题中提供更多信息. (6认同)

Jac*_*hee 11

在某些情况下,使用它是可以接受的Object.create.如果您需要反向兼容性或想要滚动自己的初始化函数,Mozilla引用包括polyfill.

适用于您的示例:

Object.create(Person.prototype, {
    'Field1': { value: 'ASD' },
    'Field2': { value: 'QWE' }
});
Run Code Online (Sandbox Code Playgroud)

有用的场景

  • 单元测试
  • 内联声明

在我的情况下,我发现这在单元测试中很有用,原因有两个:

  1. 在测试期望时,我经常想要创建一个苗条的对象作为期望
  2. 单元测试框架(如Jasmine)可以比较对象原型(__proto__)并且不通过测试.例如:
var actual = new MyClass();
actual.field1 = "ASD";
expect({ field1: "ASD" }).toEqual(actual); // fails
Run Code Online (Sandbox Code Playgroud)

单元测试失败的输出不会产生关于不匹配的线索.

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

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

var o = { 
  m: MyClass: { Field1:"ASD" }
};
Run Code Online (Sandbox Code Playgroud)


小智 5

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

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)
  }
}
Run Code Online (Sandbox Code Playgroud)

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