TypeScript and field initializers

前端 未结 14 2250
暖寄归人
暖寄归人 2020-11-30 16:49

How to init a new class in TS in such a way (example in C# to show what I want):

// ... some code before
return new MyClass { Field         


        
相关标签:
14条回答
  • 2020-11-30 17:13

    The easiest way to do this is with type casting.

    return <MyClass>{ Field1: "ASD", Field2: "QWE" };
    
    0 讨论(0)
  • 2020-11-30 17:14

    I'd be more inclined to do it this way, using (optionally) automatic properties and defaults. You haven't suggested that the two fields are part of a data structure, so that's why I chose this way.

    You could have the properties on the class and then assign them the usual way. And obviously they may or may not be required, so that's something else too. It's just that this is such nice syntactic sugar.

    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
    
    0 讨论(0)
  • 2020-11-30 17:17

    Below is a solution that combines a shorter application of Object.assign to more closely model the original C# pattern.

    But first, lets review the techniques offered so far, which include:

    1. Copy constructors that accept an object and apply that to Object.assign
    2. A clever Partial<T> trick within the copy constructor
    3. Use of "casting" against a POJO
    4. Leveraging Object.create instead of Object.assign

    Of course, each have their pros/cons. Modifying a target class to create a copy constructor may not always be an option. And "casting" loses any functions associated with the target type. Object.create seems less appealing since it requires a rather verbose property descriptor map.

    Shortest, General-Purpose Answer

    So, here's yet another approach that is somewhat simpler, maintains the type definition and associated function prototypes, and more closely models the intended C# pattern:

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

    That's it. The only addition over the C# pattern is Object.assign along with 2 parenthesis and a comma. Check out the working example below to confirm it maintains the type's function prototypes. No constructors required, and no clever tricks.

    Working Example

    This example shows how to initialize an object using an approximation of a C# field initializer:

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

    0 讨论(0)
  • 2020-11-30 17:20

    You can affect an anonymous object casted in your class type. Bonus: In visual studio, you benefit of intellisense this way :)

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

    Edit:

    WARNING If the class has methods, the instance of your class will not get them. If AClass has a constructor, it will not be executed. If you use instanceof AClass, you will get false.

    In conclusion, you should used interface and not class. The most common use is for the domain model declared as Plain Old Objects. Indeed, for domain model you should better use interface instead of class. Interfaces are use at compilation time for type checking and unlike classes, interfaces are completely removed during compilation.

    interface IModel {
       Property1: string;
       Property2: string;
       PropertyBoolean: boolean;
       PropertyNumber: number;
    }
    
    var anObject: IModel = {
         Property1: "Value",
         Property2: "Value",
         PropertyBoolean: true,
         PropertyNumber: 1
     };
    
    0 讨论(0)
  • 2020-11-30 17:22

    In some scenarios it may be acceptable to use Object.create. The Mozilla reference includes a polyfill if you need back-compatibility or want to roll your own initializer function.

    Applied to your example:

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

    Useful Scenarios

    • Unit Tests
    • Inline declaration

    In my case I found this useful in unit tests for two reasons:

    1. When testing expectations I often want to create a slim object as an expectation
    2. Unit test frameworks (like Jasmine) may compare the object prototype (__proto__) and fail the test. For example:
    var actual = new MyClass();
    actual.field1 = "ASD";
    expect({ field1: "ASD" }).toEqual(actual); // fails
    

    The output of the unit test failure will not yield a clue about what is mismatched.

    1. In unit tests I can be selective about what browsers I support

    Finally, the solution proposed at http://typescript.codeplex.com/workitem/334 does not support inline json-style declaration. For example, the following does not compile:

    var o = { 
      m: MyClass: { Field1:"ASD" }
    };
    
    0 讨论(0)
  • 2020-11-30 17:22

    I wanted a solution that would have the following:

    • All the data objects are required and must be filled by the constructor.
    • No need to provide defaults.
    • Can use functions inside the class.

    Here is the way that I do it:

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

    All the properties in the constructor are mandatory and may not be omitted without a compiler error.

    It is dependant on the OnlyData that filters out getFullName() out of the required properties and it is defined like so:

    // 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>;
    

    Current limitations of this way:

    • Requires TypeScript 2.8
    • Classes with getters/setters
    0 讨论(0)
提交回复
热议问题