TypeScript și inițializatorii de câmp
Cum să inițializezi o nouă clasă în TS în acest fel (exemplu în C# pentru a arăta ce vreau):
// ... some code before
return new MyClass { Field1 = "ASD", Field2 = "QWE" };
// ... some code after
Răspuns #1
Actualizat 12.07.2016: Typescript 2.1 introduce Tipuri Mapate și oferă Partial<T>, care vă permite să faceți acest lucru....
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"}),
];
Răspuns original:
Abordarea mea este să definesc o variabilă separată fields pe care o treci constructorului. Trucul este să redefinești toate câmpurile clasei pentru acest inițializator ca opționale. Când obiectul este creat (cu valorile sale implicite), pur și simplu aloci obiectul inițializator pe 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);
}
}
sau fă-l manual (puțin mai sigur):
if (fields) {
this.name = fields.name || this.name;
this.address = fields.address || this.address;
this.age = fields.age || this.age;
}
utilizare :
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
];
și ieșirea în consolă:
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 }
Acest lucru îți oferă siguranță de bază și inițializarea proprietăților, dar totul este opțional și poate fi în ordine arbitrară. Obții valorile implicite ale clasei lăsate neschimbate dacă nu treci un câmp.
De asemenea, îl poți combina și cu parametrii necesari ai constructorului -- adaugă fields la final.
Cam la fel de aproape de stilul C# pe cât cred că vei ajunge (sintaxa reală de inițializare a câmpului a fost respinsă). Aș prefera mult mai mult un initializer de câmp adecvat, dar nu pare să se întâmple încă.
Pentru comparație, dacă folosești abordarea de tip casting, obiectul tău inițializator trebuie să aibă TOATE câmpurile pentru tipul la care faci casting, plus să nu obții nici o funcție specifică clasei (sau derivate) creată de clasă însăși.
Răspunsul #2
Actualizare
De când am scris acest răspuns, au apărut metode mai bune. Vă rugăm să consultați celelalte răspunsuri de mai jos care au mai multe voturi și un răspuns mai bun. Nu pot să șterg acest răspuns deoarece este marcat ca acceptat.
Răspuns vechi
Există o problemă pe TypeScript Codeplex care descrie acest lucru: Suport pentru inițializatoarele de obiecte.
Așa cum s-a menționat, puteți deja face acest lucru folosind interfețe în TypeScript în loc de clase:
interface Name {
givenName: string;
surname: string;
}
class Person {
name: Name;
age: number;
}
var bob: Person = {
name: {
givenName: "Bob",
surname: "Smith",
},
age: 35,
};
Răspunsul #3
Mai jos este o soluție care combină o aplicare mai scurtă a Object.assign pentru a modela mai îndeaproape tiparul original C#.
Dar mai întâi, să revizuim tehnicile oferite până acum, care includ:
- Constructorii de copiere care acceptă un obiect și îl aplică la
Object.assign - Un truc inteligent
Partial<T>în interiorul constructorului de copiere - Utilizarea „casting”-ului împotriva unui POJO
- Folosirea
Object.createîn loc deObject.assign
Desigur, fiecare are avantajele/dezavantajele sale. Modificarea unei clase țintă pentru a crea un constructor de copiere poate să nu fie întotdeauna o opțiune. Și „casting-ul” pierde orice funcții asociate cu tipul țintă. Object.create pare mai puțin atractiv deoarece necesită o hartă de descriptor de proprietăți destul de detaliată.
Răspuns scurt, general
Așadar, iată încă o abordare care este oarecum mai simplă, păstrează definiția tipului și prototipurile funcțiilor asociate și modelează mai îndeaproape modelul C# intenționat:
const john = Object.assign( new Person(), {
name: "John",
age: 29,
address: "Earth"
});
Asta este. Singura adiție față de modelul C# este Object.assign împreună cu 2 paranteze și o virgulă. Verifică exemplul funcțional de mai jos pentru a confirma că păstrează prototipurile funcțiilor tipului. Nu sunt necesare constructori și nici trucuri ingenioase.
Exemplu funcțional
Acest exemplu arată cum să inițializezi un obiect folosind o aproximare a unui initializer de câmp 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() );Răspunsul #4
Poți afecta un obiect anonim convertit în tipul clasei tale. Bonus: În Visual Studio, beneficiezi de IntelliSense în acest mod :)
var anInstance: AClass = <AClass> {
Property1: "Value",
Property2: "Value",
PropertyBoolean: true,
PropertyNumber: 1
};
Editare :
ATENȚIONARE Dacă clasa are metode, instanța clasei tale nu le va primi. Dacă AClass are un constructor, acesta nu va fi executat. Dacă folosești instanceof AClass, vei obține false.
În concluzie, ar trebui să folosești interfața și nu clasa. Cea mai comună utilizare este pentru modelul de domeniu declarat ca obiecte simple. De fapt, pentru modelul de domeniu ar fi mai bine să folosești interfața în loc de clasă. Interfețele sunt folosite la momentul compilării pentru verificarea tipurilor și, spre deosebire de clase, interfețele sunt complet eliminate în timpul compilării.
interface IModel {
Property1: string;
Property2: string;
PropertyBoolean: boolean;
PropertyNumber: number;
}
var anObject: IModel = {
Property1: "Value",
Property2: "Value",
PropertyBoolean: true,
PropertyNumber: 1
};
Răspunsul #5
Sugerez o abordare care nu necesită 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();
puncte cheie:
- Typescript 2.1 nu este necesar,
Partial<T>nu este necesar - Suportă funcții (în comparație cu afirmația simplă de tip care nu suportă funcții)
Răspuns #6
În unele scenarii poate fi acceptabil să folosiți Object.create. Referința Mozilla include un polyfill dacă aveți nevoie de compatibilitate înapoi sau doriți să vă creați propria funcție de inițializare.
Aplicat la exemplul tău:
Object.create(Person.prototype, {
'Field1': { value: 'ASD' },
'Field2': { value: 'QWE' }
});
Scenarii utile
- Teste unitare
- Declarație inline
În cazul meu, am găsit acest lucru util în testele unitare din două motive:
- Când testez așteptările, adesea vreau să creez un obiect subțire ca așteptare
- Framework-urile de teste unitare (cum ar fi Jasmine) pot compara prototipul obiectului (
__proto__) și pot eșua testul. De exemplu:
var actual = new MyClass();
actual.field1 = "ASD";
expect({ field1: "ASD" }).toEqual(actual); // fails
Eșecul testului unitar nu va oferi indicii despre ceea ce nu se potrivește.
- În testele unitare pot fi selectiv în privința browserelor pe care le suport
În cele din urmă, soluția propusă la http://typescript.codeplex.com/workitem/334 nu suportă declarația inline în stil JSON. De exemplu, următorul cod nu se compilează:
var o = {
m: MyClass: { Field1:"ASD" }
};
Răspunsul #7
Am vrut o soluție care să aibă următoarele:
- Toate obiectele de date sunt necesare și trebuie completate de constructor.
- Nu este nevoie să oferiți valori implicite.
- Poate folosi funcții în interiorul clasei.
Iată modul în care o fac:
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();
Toate proprietățile din constructor sunt obligatorii și nu pot fi omise fără a provoca o eroare de compilare.
Este dependent de OnlyData care filtrează getFullName() din proprietățile necesare și este definit astfel:
// 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>;
Limitările actuale ale acestei metode:
- Necesită TypeScript 2.8
- Clase cu getteri/setteri
Răspunsul #8
Aș fi mai înclinat să fac asta în acest mod, folosind (opțional) proprietăți și valori implicite automate. Nu ai sugerat că cele două câmpuri fac parte dintr-o structură de date, așa că de aceea am ales această variantă.
Ai putea avea proprietățile în clasă și apoi să le aloci într-un mod obișnuit. Și evident, ele pot sau nu să fie obligatorii, deci asta este altceva de asemenea. Numai că acesta este un asemenea zahăr sintactic plăcut.
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
Răspunsul #9
Ai putea avea o clasă cu câmpuri opționale (marcate cu ?) și un constructor care primește o instanță a aceleași clase.
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" }),
];
În acest caz, nu veți putea omite câmpurile necesare. Acest lucru vă oferă un control detaliat asupra construcției obiectului.
Ai putea folosi constructorul cu tipul Partial, așa cum s-a menționat în alte răspunsuri:
public constructor(init?:Partial<Person>) {
Object.assign(this, init);
}
Problema este că toate câmpurile devin opționale și acest lucru nu este de dorit în majoritatea cazurilor.
