TypeScript- und Feldinitialisierer


251

So initiieren Sie eine neue Klasse auf TSdiese Weise (Beispiel in, um C#zu zeigen, was ich will):

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

[Bearbeiten]
Als ich diese Frage schrieb, war ich ein reiner .NET-Entwickler ohne viel JS-Wissen. Auch TypeScript war etwas völlig Neues, das als neue C # -basierte Obermenge von JavaScript angekündigt wurde. Heute sehe ich, wie dumm diese Frage war.

Wenn noch jemand nach einer Antwort sucht, schauen Sie sich bitte die folgenden möglichen Lösungen an.

Als erstes sollten wir in TS keine leeren Klassen für Modelle erstellen. Besser ist es, eine Schnittstelle oder einen Typ zu erstellen (je nach Bedarf). Guter Artikel von Todd Motto: https://ultimatecourses.com/blog/classes-vs-interfaces-in-typescript

LÖSUNG 1:

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

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

LÖSUNG 2:

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

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

LÖSUNG 3 (wenn Sie wirklich eine Klasse brauchen):

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

oder

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

In beiden Fällen benötigen Sie Casting-Typen möglicherweise nicht manuell, da sie vom Funktions- / Methodenrückgabetyp aufgelöst werden.


9
Die "Lösung", die Sie gerade an Ihre Frage angehängt haben, ist kein gültiges TypeScript oder JavaScript. Es ist jedoch wertvoll darauf hinzuweisen, dass es am intuitivsten ist, es zu versuchen.
Jacob Foshee

1
@ JacobFoshee nicht gültiges JavaScript? Siehe meine Chrome Dev Tools: i.imgur.com/vpathu6.png (aber Visual Studio Code oder andere TypeScript-Flusen würden sich unweigerlich beschweren)
Mars Robertson

5
@MichalStefanow das OP hat die Frage bearbeitet, nachdem ich diesen Kommentar gepostet habe. Er hattereturn new MyClass { Field1: "ASD", Field2: "QWE" };
Jacob Foshee

Antworten:


90

Aktualisieren

Seit dem Schreiben dieser Antwort sind bessere Wege gekommen. Bitte beachten Sie die anderen Antworten unten, die mehr Stimmen und eine bessere Antwort haben. Ich kann diese Antwort nicht entfernen, da sie als akzeptiert markiert ist.


Alte Antwort

Beim TypeScript-Codeplex gibt es ein Problem, das dies beschreibt: Unterstützung für Objektinitialisierer .

Wie bereits erwähnt, können Sie dies bereits tun, indem Sie Schnittstellen in TypeScript anstelle von Klassen verwenden:

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

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

81
In Ihrem Beispiel ist Bob keine Instanz der Klasse Person. Ich sehe nicht, wie dies dem C # -Beispiel entspricht.
Jack Wester

3
Schnittstellennamen sind besser mit Großbuchstaben "I" zu beginnen
Kiarash

16
1. Person ist keine Klasse und 2. @JackWester hat recht, Bob ist keine Instanz von Person. Versuchen Sie Alarm (Bob Instanz der Person); In diesem Codebeispiel dient Person nur zu Zwecken der Typzusicherung.
Jacques

15
Ich stimme Jack und Jaques zu und ich denke, es lohnt sich zu wiederholen. Ihr Bob ist vom Typ Person , aber es ist überhaupt keine Instanz von Person. Stellen Sie sich vor, dass dies Persontatsächlich eine Klasse mit einem komplexen Konstruktor und einer Reihe von Methoden wäre. Dieser Ansatz würde auf den Kopf fallen. Es ist gut, dass eine Menge Leute Ihren Ansatz nützlich fanden, aber es ist keine Lösung für die Frage, wie es angegeben ist, und Sie könnten in Ihrem Beispiel genauso gut eine Klasse Person anstelle einer Schnittstelle verwenden, es wäre der gleiche Typ.
Alex

26
Warum ist dies eine akzeptierte Antwort, wenn sie eindeutig falsch ist?
Hendrik Wiese

447

Aktualisiert 07/12/2016: Typescript 2.1 führt Mapped Types ein und bietet Partial<T>, was Ihnen dies ermöglicht ....

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"}),
];

Ursprüngliche Antwort:

Mein Ansatz besteht darin, eine separate fieldsVariable zu definieren , die Sie an den Konstruktor übergeben. Der Trick besteht darin, alle Klassenfelder für diesen Initialisierer als optional neu zu definieren. Wenn das Objekt erstellt wird (mit seinen Standardeinstellungen), weisen Sie einfach das Initialisiererobjekt zu 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);
    }
}

oder manuell (etwas sicherer):

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

Verwendung:

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

und Konsolenausgabe:

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 }   

Dies bietet Ihnen grundlegende Sicherheits- und Eigenschaftsinitialisierung, ist jedoch optional und kann außer Betrieb sein. Sie erhalten die Standardeinstellungen der Klasse in Ruhe, wenn Sie kein Feld passieren.

Sie können es auch mit den erforderlichen Konstruktorparametern mischen - bleiben Sie fieldsam Ende.

Etwa so nah wie möglich am C # -Stil ( tatsächliche Field-Init-Syntax wurde abgelehnt ). Ich würde einen richtigen Feldinitialisierer sehr bevorzugen, aber es sieht noch nicht so aus, als würde es passieren.

Zum Vergleich: Wenn Sie den Casting-Ansatz verwenden, muss Ihr Initialisiererobjekt ALLE Felder für den Typ enthalten, in den Sie gießen, und es dürfen keine klassenspezifischen Funktionen (oder Ableitungen) von der Klasse selbst erstellt werden.


1
+1. Dadurch wird tatsächlich eine Instanz einer Klasse erstellt (was bei den meisten dieser Lösungen nicht der Fall ist), alle Funktionen im Konstruktor beibehalten (kein Object.create oder andere Cruft außerhalb) und der Eigenschaftsname explizit angegeben, anstatt sich auf die Parameterreihenfolge zu verlassen ( meine persönliche Präferenz hier). Es verliert jedoch die Typ- / Kompilierungssicherheit zwischen den Parametern und Eigenschaften.
Geigen

1
@ user1817787 Sie sind wahrscheinlich besser dran, alles, was einen Standard hat, als optional in der Klasse selbst zu definieren, aber einen Standard zuzuweisen. Dann nutzen Sie nicht Partial<>nur Person- das erfordert , dass Sie ein Objekt zu übergeben , die die erforderlichen Felder. dass das, siehe hier für Ideen (siehe Auswahl) diese Grenze Typzuordnung auf bestimmte Felder aus .
Meirion Hughes

2
Das sollte wirklich die Antwort sein. Es ist die beste Lösung für dieses Problem.
Ascherer

9
das ist GOLDEN Stück Codepublic constructor(init?:Partial<Person>) { Object.assign(this, init); }
kuncevic.dev

3
Wenn Object.assign einen Fehler anzeigt, wie er für mich war, lesen
Jim Yarbro

27

Im Folgenden finden Sie eine Lösung, die eine kürzere Anwendung kombiniert, Object.assignum das ursprüngliche C#Muster genauer zu modellieren .

Lassen Sie uns zunächst die bisher angebotenen Techniken überprüfen, darunter:

  1. Kopieren Sie Konstruktoren, die ein Objekt akzeptieren, und wenden Sie diese an Object.assign
  2. Ein cleverer Partial<T>Trick im Kopierkonstruktor
  3. Verwendung von "Casting" gegen ein POJO
  4. Hebelwirkung Object.createstattObject.assign

Natürlich hat jeder seine Vor- und Nachteile. Das Ändern einer Zielklasse zum Erstellen eines Kopierkonstruktors ist möglicherweise nicht immer eine Option. Und "Casting" verliert alle Funktionen, die dem Zieltyp zugeordnet sind. Object.createscheint weniger ansprechend zu sein, da eine ziemlich ausführliche Eigenschaftsbeschreibungskarte erforderlich ist.

Kürzeste allgemeine Antwort

Hier ist also ein weiterer Ansatz, der etwas einfacher ist, die Typdefinition und die zugehörigen Funktionsprototypen beibehält und das beabsichtigte C#Muster genauer modelliert :

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

Das ist es. Der einzige Zusatz über dem C#Muster ist Object.assignzusammen mit 2 Klammern und einem Komma. Sehen Sie sich das folgende Arbeitsbeispiel an, um zu bestätigen, dass die Funktionsprototypen des Typs beibehalten werden. Keine Konstruktoren erforderlich und keine cleveren Tricks.

Arbeitsbeispiel

Dieses Beispiel zeigt, wie ein Objekt mithilfe einer Approximation eines C#Feldinitialisierers initialisiert wird:

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


Auch dieses kann ein Benutzerobjekt aus einem Javascript-Objekt erstellen
Dave Keane

1
6½ Jahre später habe ich das vergessen, aber es gefällt mir immer noch. Danke noch einmal.
PRMan

25

Sie können ein anonymes Objekt beeinflussen, das in Ihrem Klassentyp umgewandelt wurde. Bonus : Im Visual Studio profitieren Sie auf diese Weise von Intellisense :)

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

Bearbeiten:

WARNUNG Wenn die Klasse über Methoden verfügt, werden diese von der Instanz Ihrer Klasse nicht abgerufen. Wenn AClass einen Konstruktor hat, wird dieser nicht ausgeführt. Wenn Sie eine Instanz von AClass verwenden, erhalten Sie false.

Abschließend sollten Sie die Schnittstelle und nicht die Klasse verwenden . Die häufigste Verwendung ist das Domänenmodell, das als einfache alte Objekte deklariert ist. In der Tat sollten Sie für das Domänenmodell besser die Schnittstelle anstelle der Klasse verwenden. Schnittstellen werden zur Kompilierungszeit zur Typprüfung verwendet. Im Gegensatz zu Klassen werden Schnittstellen während der Kompilierung vollständig entfernt.

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

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

20
Wenn AClassMethoden enthalten anInstancewären , würden sie nicht erhalten.
Monsignore

2
Auch wenn AClass einen Konstruktor hat, wird dieser nicht ausgeführt.
Michael Erickson

1
Auch wenn Sie dies tun, erhalten anInstance instanceof AClassSie falsezur Laufzeit.
Lucero

15

Ich schlage einen Ansatz vor, für den Typescript 2.1 nicht erforderlich ist:

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

Schlüsselpunkte:

  • Typoskript 2.1 nicht erforderlich, Partial<T>nicht erforderlich
  • Es unterstützt Funktionen (im Vergleich zur einfachen Typzusicherung, die keine Funktionen unterstützt)

1
Pflichtfelder werden nicht berücksichtigt: new Person(<Person>{});(weil Casting) und auch klar zu sein; Die Verwendung von Partial <T> unterstützt Funktionen. Wenn Sie Felder (plus Prototypfunktionen) benötigt haben, müssen Sie letztendlich Folgendes tun: init: { name: string, address?: string, age: number }und die Besetzung löschen.
Meirion Hughes

Auch wenn wir eine bedingte Typzuordnung erhalten, können Sie nur die Funktionen Partials zuordnen und die Eigenschaften unverändert lassen. :)
Meirion Hughes

Anstelle des Anspruchsvollen kompiliert und funktioniert es classdann var, wenn ich es tue var dog: {name: string} = {name: 'will be assigned later'};. Irgendwelche Mängel oder Probleme? Oh, doghat einen sehr kleinen Umfang und ist spezifisch, was nur eine Instanz bedeutet.
Jeb50

11

Ich würde eher dazu neigen, dies zu tun und (optional) automatische Eigenschaften und Standardeinstellungen zu verwenden. Sie haben nicht vorgeschlagen, dass die beiden Felder Teil einer Datenstruktur sind. Deshalb habe ich diesen Weg gewählt.

Sie können die Eigenschaften für die Klasse haben und sie dann auf die übliche Weise zuweisen. Und natürlich können sie erforderlich sein oder auch nicht, also ist das auch etwas anderes. Es ist nur so, dass dies so schöner syntaktischer Zucker ist.

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
Ich entschuldige mich tief. Sie haben jedoch nicht "nach Feldinitialisierern gefragt", daher ist es selbstverständlich anzunehmen, dass Sie an alternativen Möglichkeiten interessiert sind, eine Klasse in TS neu zu erstellen. Sie könnten ein wenig mehr Informationen in Ihrer Frage geben, wenn Sie so bereit sind, abzustimmen.
Ralph Lavelle

+1 Konstruktor ist der Weg, wo es möglich ist; Aber in Fällen, in denen Sie viele Felder haben und nur einige davon initialisieren möchten, denke ich, dass meine Antwort die Dinge einfacher macht.
Meirion Hughes

1
Wenn Sie viele Felder haben, wäre das Initialisieren eines Objekts wie dieses ziemlich unhandlich, da Sie dem Konstruktor eine Wand mit namenlosen Werten übergeben würden. Das heißt nicht, dass diese Methode keinen Wert hat; Es ist am besten für einfache Objekte mit wenigen Feldern geeignet. Die meisten Veröffentlichungen sagen, dass ungefähr vier oder fünf Parameter in der Signatur eines Mitglieds die Obergrenze sind. Ich möchte nur darauf hinweisen, weil ich bei der Suche nach TS-Initialisierern in einem Blog eines anderen einen Link zu dieser Lösung gefunden habe. Ich habe Objekte mit mehr als 20 Feldern, die für Unit-Tests initialisiert werden müssen.
Dustin Cleveland

11

In einigen Szenarien kann die Verwendung akzeptabel sein Object.create. Die Mozilla-Referenz enthält eine Polyfüllung, wenn Sie eine Rückkompatibilität benötigen oder Ihre eigene Initialisierungsfunktion ausführen möchten.

Auf Ihr Beispiel angewendet:

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

Nützliche Szenarien

  • Unit Tests
  • Inline-Erklärung

In meinem Fall fand ich dies in Unit-Tests aus zwei Gründen nützlich:

  1. Beim Testen von Erwartungen möchte ich oft ein schlankes Objekt als Erwartung erstellen
  2. Unit-Test-Frameworks (wie Jasmine) können den Objektprototyp ( __proto__) vergleichen und den Test nicht bestehen. Beispielsweise:
var actual = new MyClass();
actual.field1 = "ASD";
expect({ field1: "ASD" }).toEqual(actual); // fails

Die Ausgabe des Unit-Test-Fehlers gibt keinen Hinweis darauf, was nicht übereinstimmt.

  1. In Unit-Tests kann ich selektiv auswählen, welche Browser ich unterstütze

Schließlich unterstützt die unter http://typescript.codeplex.com/workitem/334 vorgeschlagene Lösung keine Inline-Deklaration im json-Stil. Folgendes wird beispielsweise nicht kompiliert:

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

9

Ich wollte eine Lösung, die Folgendes hätte:

  • Alle Datenobjekte sind erforderlich und müssen vom Konstruktor ausgefüllt werden.
  • Es müssen keine Standardeinstellungen angegeben werden.
  • Kann Funktionen innerhalb der Klasse verwenden.

So mache ich das:

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

Alle Eigenschaften im Konstruktor sind obligatorisch und dürfen ohne einen Compilerfehler nicht weggelassen werden.

Es hängt davon ab, OnlyDatadass aus getFullName()den erforderlichen Eigenschaften herausgefiltert wird, und es ist wie folgt definiert:

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

Aktuelle Einschränkungen dieser Art:

  • Benötigt TypeScript 2.8
  • Klassen mit Getter / Setter

Diese Antwort scheint mir dem Ideal am nächsten zu sein.
Ich frage

1
Soweit ich das beurteilen kann, ist dies bis heute der beste Weg nach vorne. Danke dir.
Florian Norbert Bepunkt

Bei dieser Lösung gibt es ein Problem: @VitalyB: Sobald die Methoden Parameter haben, wird Folgendes unterbrochen: Während getFullName () {return "bar"} funktioniert, funktioniert getFullName (str: string): string {return str} nicht
florian norbert bepunkt

@floriannorbertbepunkt Was genau funktioniert bei Ihnen nicht? Es scheint gut für mich zu funktionieren ...
rfgamaral

3

Dies ist eine andere Lösung:

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

2

Sie könnten eine Klasse mit optionalen Feldern (markiert mit?) Und einen Konstruktor haben, der eine Instanz derselben Klasse empfängt.

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" }),
];

In diesem Fall können Sie die erforderlichen Felder nicht auslassen. Auf diese Weise können Sie die Objektkonstruktion genau steuern.

Sie können den Konstruktor mit dem Partial-Typ verwenden, wie in anderen Antworten angegeben:

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

Das Problem ist, dass alle Felder optional werden und dies in den meisten Fällen nicht wünschenswert ist.


1

Der einfachste Weg, dies zu tun, ist das Typgießen.

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

7
Leider ist (1) dies kein Typ-Casting, sondern eine Typ- Behauptung zur Kompilierungszeit , (2) die Frage "Wie kann eine neue Klasse initiiert werden? " (Hervorhebung von mir), und dieser Ansatz wird dies nicht erreichen. Es wäre sicherlich schön, wenn TypeScript diese Funktion hätte, aber das ist leider nicht der Fall.
John Weisz

Wo ist die Definition von MyClass?
Jeb50

0

Wenn Sie eine alte Version von Typoskript <2.1 verwenden, können Sie eine ähnliche Version wie die folgende verwenden, bei der im Grunde ein beliebiges typisiertes Objekt umgewandelt wird:

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

HINWEIS: Die Verwendung dieser Methode ist nur für Datenmodelle geeignet, da alle Methoden im Objekt entfernt werden. Grundsätzlich wird jedes Objekt in ein typisiertes Objekt umgewandelt


-1

Wenn Sie eine neue Instanz erstellen möchten, ohne den Anfangswert für die Instanz festzulegen

1- Sie müssen Klasse nicht Schnittstelle verwenden

2- Sie müssen den Anfangswert festlegen, wenn Sie eine Klasse erstellen

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


student: IStudentDTO = new IStudentDTO();
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.