【问题标题】:TypeScript and field initializersTypeScript 和字段初始化器
【发布时间】:2012-12-18 00:02:50
【问题描述】:

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

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

【问题讨论】:

  • 您刚刚附加到问题的“解决方案”不是有效的 TypeScript 或 JavaScript。但值得指出的是,这是最直观的尝试。
  • @JacobFoshee JavaScript 无效?请参阅我的 Chrome 开发工具:i.imgur.com/vpathu6.png (但 Visual Studio Code 或任何其他 TypeScript linted 将不可避免地抱怨)
  • @MichalStefanow OP 在我发布该评论后编辑了该问题。他确实有return new MyClass { Field1: "ASD", Field2: "QWE" };
  • 请不要将答案放在问题中 - 如果您有答案,请写在下面。

标签: typescript


【解决方案1】:

更新

自从写下这个答案以来,已经出现了更好的方法。请参阅下面有更多投票和更好答案的其他答案。我无法删除此答案,因为它已被标记为已接受。


旧答案

TypeScript codeplex 上存在一个描述此问题的问题:Support for object initializers

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

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

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

【讨论】:

  • 在您的示例中,bob 不是 Person 类的实例。我看不出这与 C# 示例有何等价。
  • “Bob”为什么不是“Person”实例?对我来说,它显然是将 bob 声明为 Person 并为实例提供属性。
  • 接口名称最好以大写“I”开头
  • 1. Person 不是一个类和 2。@JackWester 是对的,bob 不是 Person 的 instanceof。试试 alert(bob instanceof Person);在此代码示例中,Person 仅用于类型断言目的。
  • 我同意 Jack 和 Jaques 的观点,我认为值得重复。您的 bob 属于 type Person,但它根本不是 Person 的实例。想象一下,Person 实际上将是一个具有复杂构造函数和一堆方法的类,这种方法将一蹶不振。很多人发现您的方法很有用,这很好,但这并不是问题的解决方案,因为它是陈述的,您也可以在示例中使用类 Person 而不是接口,它将是相同的类型。跨度>
【解决方案2】:

这是我找到的最佳解决方案。

声明一个可以用作装饰器的函数。我称之为自动反射

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# 对象初始化的感觉。

【讨论】:

  • 似乎不再工作了
【解决方案3】:

对于更现代的 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,
      },
      {
      . . . .
      }
    ];

【讨论】:

    【解决方案4】:

    这是一个解决方案:

    • 不会强制您将所有字段设为可选(与 Partial&lt;...&gt; 不同)
    • 区分类方法和函数类型的字段(与OnlyData&lt;...&gt; 解决方案不同)
    • 通过定义 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;
    

    【讨论】:

      【解决方案5】:

      我建议一种不需要 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&lt;T&gt; 不需要
      • 支持函数(与不支持函数的简单类型断言相比)

      【讨论】:

      • 它不尊重必填字段:new Person(&lt;Person&gt;{});(因为强制转换)并且也很清楚;使用 Partial 支持函数。最终,如果您有必填字段(加上原型函数),您需要这样做:init: { name: string, address?: string, age: number } 并删除演员表。
      • 另外when we get conditional type mapping 您将能够仅将函数映射到部分,并保持属性不变。 :)
      • 而不是复杂的class 然后var,如果我这样做var dog: {name: string} = {name: 'will be assigned later'};,它会编译并工作。有什么不足或问题吗?哦,dog 的范围非常小而且具体,意味着只有一个实例。
      【解决方案6】:

      在不重新声明所有默认属性的情况下初始化一个类:

      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)
        }
      }
      

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

      【讨论】:

      • 如何调用这个构造函数? const opts: { prop1: 'helloworld' } new MyClass(opts ???);
      • @DanielMethner 通过调用 MyClass({ prop1:"foo", props2:"bar" }) 来创建
      【解决方案7】:

      这是另一种解决方案:

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

      【讨论】:

      • 这样的话,能否保证类的构造函数已经执行?
      【解决方案8】:

      如果你想创建新的实例而不设置实例时的初始值

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

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

      export class IStudentDTO {
       Id:        number = 0;
       Name:      string = '';
      
      
      student: IStudentDTO = new IStudentDTO();
      

      【讨论】:

        【解决方案9】:

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

        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);
        }
        

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

        【讨论】:

        • 这个答案的问题是,如果Person类有方法,那么构造函数的参数也应该有方法。
        【解决方案10】:

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

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

        这是我的做法:

        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 的类

        【讨论】:

        • 这个答案对我来说似乎最接近理想。想知道我们是否可以用 typescript 3+ 做得更好
        • 据我所知,这是目前最好的方法。谢谢。
        • 这个解决方案有一个问题,@VitalyB:只要方法有参数,就会中断:虽然 getFullName() { return "bar" } 有效,但 getFullName(str: string):string { return str } 不起作用
        • @floriannorbertbepunkt 到底什么不适合您?它似乎对我来说工作正常......
        • 这适用于大多数情况!如果其中一个类字段是函数类型,则会出现问题,因为它也会被忽略。
        【解决方案11】:

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

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

        编辑:

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

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

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

        【讨论】:

        • 如果AClass 包含方法,anInstance 不会得到它们。
        • 另外如果AClass有构造函数,也不会被执行。
        • 另外,如果您执行anInstance instanceof AClass,您将在运行时获得false
        • 这不符合@Nikron 的要求。这不会实例化一个类。它创建一个对象,然后告诉 TS 它是所说的类。这与说({} as any as AClass) 没什么不同
        【解决方案12】:

        如果您使用的是旧版本的 typescript

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

        注意:使用此方法仅适用于数据模型,因为它会删除 对象中的所有方法。它基本上是将任何对象投射到 类型化对象

        【讨论】:

          【解决方案13】:

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

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

          1. 复制接受对象的构造函数并将其应用于Object.assign
          2. 复制构造函数中的巧妙 Partial&lt;T&gt; 技巧
          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对象创建一个用户对象
          • 6½ 年后我忘记了这一点,但我仍然喜欢它。再次感谢。
          【解决方案14】:

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

          【讨论】:

            【解决方案15】:

            2016 年 7 月 12 日更新: Typescript 2.1 introduces Mapped Types 并提供Partial&lt;T&gt;,它允许你这样做......

            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# 风格差不多 (actual field-init syntax was rejected)。我更喜欢适当的字段初始化程序,但看起来它不会发生。

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

            【讨论】:

            • +1。这实际上创建了一个类的实例(这些解决方案中的大多数都没有),将所有功能保留在构造函数中(没有 Object.create 或其他杂物),并显式声明属性名称而不是依赖参数排序(我个人的偏好在这里)。不过,它确实会失去参数和属性之间的类型/编译安全性。
            • @user1817787 您最好在类本身中将任何具有默认值的东西定义为可选,但分配一个默认值。然后不要使用Partial&lt;&gt;,只使用Person - 这将要求您传递一个具有所需字段的对象。也就是说,see here 用于将类型映射限制为某些字段的想法(请参阅 Pick)。
            • 这确实应该是答案。这是解决这个问题的最佳方法。
            • 那是一段黄金代码public constructor(init?:Partial&lt;Person&gt;) { Object.assign(this, init); }
            • 如果 Object.assign 显示像我一样的错误,请查看这个 SO 答案:stackoverflow.com/a/38860354/2621693
            【解决方案16】:

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

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

            【讨论】:

            • 不幸的是,(1)这不是类型转换,而是编译时类型-断言,(2)问的问题“如何初始化一个new class" (强调我的),这种方法将无法实现这一点。如果 TypeScript 有这个功能当然很好,但不幸的是,事实并非如此。
            • MyClass的定义在哪里?
            【解决方案17】:

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

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

            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
            

            【讨论】:

            • 我深表歉意。但是您实际上并没有“询问字段初始值设定项”,因此很自然地假设您可能对在 TS 中新建一个类的替代方法感兴趣。如果您准备好投反对票,您可能会在问题中提供更多信息。
            • +1 构造函数是可行的方法;但如果您有很多字段并且只想初始化其中一些字段,我认为我的回答会让事情变得更容易。
            • 如果你有很多字段,像这样初始化一个对象会非常笨拙,因为你会给构造函数传递一堵无名值的墙。这并不是说这种方法没有优点。它最适合用于具有少量字段的简单对象。大多数出版物说,成员签名中的大约四个或五个参数是上限。之所以指出这一点,是因为我在搜索 TS 初始化程序时在某人的博客上找到了该解决方案的链接。我有 20 多个字段的对象需要为单元测试初始化​​。
            猜你喜欢
            • 1970-01-01
            • 2010-12-27
            • 2013-03-30
            • 2010-10-25
            • 1970-01-01
            • 2014-04-01
            • 1970-01-01
            • 2014-10-06
            • 2012-10-19
            相关资源
            最近更新 更多