TypeScript och fältinitialiseringar
Hur man initierar en ny klass i TS på ett sådant sätt (exempel i C# för att visa vad jag menar):
// ... some code before
return new MyClass { Field1 = "ASD", Field2 = "QWE" };
// ... some code after
Svar #1
Uppdaterad 2016-12-07: Typescript 2.1 introducerar Mappade Typer och tillhandahåller Partial<T>, vilket tillåter dig att göra detta....
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"}),
];
Originalt svar:
Min metod är att definiera en separat fields-variabel som du skickar till konstruktorn. Tricket är att omdefiniera alla klassfält för denna initialiserare som valfria. När objektet skapas (med dess standardvärden) tilldelar du helt enkelt initialiserarobjektet till 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);
}
}
eller gör det manuellt (lite säkrare):
if (fields) {
this.name = fields.name || this.name;
this.address = fields.address || this.address;
this.age = fields.age || this.age;
}
användning :
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
];
och konsolutdata:
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 }
Detta ger dig grundläggande säkerhet och egenskapsinitiering, men allt är valfritt och kan vara i vilken ordning som helst. Du får klassens standardvärden kvar oförändrade om du inte skickar ett fält.
Du kan också blanda det med nödvändiga konstruktörsparametrar också - lägg fields i slutet.
Omkring så nära C#-stil som du kommer att få, tror jag (den faktiska fältsyntaxen för initiering avvisades). Jag skulle föredra en riktig fältinitialiserare, men det ser inte ut som det kommer att hända än.
Som jämförelse, om du använder castningsmetoden måste ditt initialiseringsobjekt ha ALLA fält för den typ du castar till, och du får inte några klassspecifika funktioner (eller härledningar) skapade av klassen själv.
Svar #2
Uppdatera
Sedan jag skrev detta svar har bättre sätt dykt upp. Vänligen se de andra svaren nedan som har fler röster och ett bättre svar. Jag kan inte ta bort detta svar eftersom det är markerat som accepterat.
Gammalt svar
Det finns ett problem på TypeScript Codeplex som beskriver detta: Stöd för objektinitierare.
Som sagt, du kan redan göra detta genom att använda gränssnitt i TypeScript istället för klasser:
interface Name {
givenName: string;
surname: string;
}
class Person {
name: Name;
age: number;
}
var bob: Person = {
name: {
givenName: "Bob",
surname: "Smith",
},
age: 35,
};
Svar #3
Nedan är en lösning som kombinerar en kortare användning av Object.assign för att närmare efterlikna det ursprungliga C#-mönstret.
Men först, låt oss gå igenom de tekniker som hittills har erbjudits, vilka inkluderar:
- Kopieringskonstruktörer som accepterar ett objekt och tillämpar det på
Object.assign - Ett smart
Partial<T>-knep inom kopieringskonstruktorn - Användning av "casting" mot en POJO
- Att använda
Object.createistället förObject.assign
Självklart har varje metod sina för- och nackdelar. Att modifiera en målklass för att skapa en kopieringskonstruktor är inte alltid ett alternativ. Och "casting" förlorar alla funktioner som är associerade med måltypen. Object.create verkar mindre attraktivt eftersom det kräver en ganska omfattande egenskapsbeskrivarkarta.
Kortaste, allmänt användbar svar
Så, här är ännu ett tillvägagångssätt som är något enklare, behåller typen definition och tillhörande funktionsprototyper, och mer noggrant modellerar det avsedda C#-mönstret:
const john = Object.assign( new Person(), {
name: "John",
age: 29,
address: "Earth"
});
Det är allt. Den enda tillägget jämfört med C#-mönstret är Object.assign tillsammans med 2 parenteser och ett kommatecken. Kolla in det fungerande exemplet nedan för att bekräfta att det bevarar typens funktionsprototyper. Inga konstruktörer krävs, och inga smarta knep.
Arbetande exempel
Detta exempel visar hur man initialiserar ett objekt med hjälp av en approximation av en C#-fältinitializer:
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() );Svar #4
Du kan påverka ett anonymt objekt som kastats till din klastyp. Bonus: I Visual Studio drar du nytta av intellisense på detta sätt :)
var anInstance: AClass = <AClass> {
Property1: "Value",
Property2: "Value",
PropertyBoolean: true,
PropertyNumber: 1
};
Redigera :
VARNING Om klassen har metoder kommer instansen av din klass inte att få dem. Om AClass har en konstruktor kommer den inte att köras. Om du använder instanceof AClass kommer du att få false.
Sammanfattningsvis bör du använda interface och inte klass. Den vanligaste användningen är för domänmodellen som deklareras som Plain Old Objects. Faktiskt, för domänmodellen bör du hellre använda interface istället för klass. Interface används vid kompilering för typkontroll och till skillnad från klasser tas interface helt bort under kompileringen.
interface IModel {
Property1: string;
Property2: string;
PropertyBoolean: boolean;
PropertyNumber: number;
}
var anObject: IModel = {
Property1: "Value",
Property2: "Value",
PropertyBoolean: true,
PropertyNumber: 1
};
Svar #5
Jag föreslår en metod som inte kräver 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();
nyckelpunkter:
- Typescript 2.1 krävs inte,
Partial<T>krävs inte - Det stöder funktioner (jämfört med enkel typkontroll som inte stöder funktioner)
Svar #6
I vissa scenarier kan det vara acceptabelt att använda Object.create. Mozillas referens inkluderar en polyfill om du behöver bakåtkompatibilitet eller vill göra din egen initialiseringsfunktion.
Tillämpat på ditt exempel:
Object.create(Person.prototype, {
'Field1': { value: 'ASD' },
'Field2': { value: 'QWE' }
});
Användbara scenarier
- Enhetstester
- Inlinedeklaration
I mitt fall fann jag detta användbart i enhetstester av två skäl:
- När jag testar förväntningar vill jag ofta skapa ett slimmat objekt som en förväntning
- Enhetstest-ramverk (som Jasmine) kan jämföra objektprototypen (
__proto__) och misslyckas med testet. Till exempel:
var actual = new MyClass();
actual.field1 = "ASD";
expect({ field1: "ASD" }).toEqual(actual); // fails
Resultatet av enhetstestfel kommer inte att ge någon ledtråd om vad som inte matchar.
- I enhetstester kan jag vara selektiv om vilka webbläsare jag stöder
Slutligen stöder inte lösningen som föreslås på http://typescript.codeplex.com/workitem/334 inline JSON-stil deklaration. Till exempel kompilerar inte följande:
var o = {
m: MyClass: { Field1:"ASD" }
};
Svar #7
Jag ville ha en lösning som skulle ha följande:
- Alla dataobjekt krävs och måste fyllas i av konstruktören.
- Ingen anledning att ange standardvärden.
- Kan använda funktioner inuti klassen.
Här är sättet som jag gör det på:
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();
Alla egenskaper i konstruktören är obligatoriska och får inte utelämnas utan ett kompileringsfel.
Det är beroende av OnlyData som filtrerar bort getFullName() från de nödvändiga egenskaperna och det definieras på följande sätt:
// 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>;
Nuvarande begränsningar med detta sätt:
- Kräver TypeScript 2.8
- Klasser med getters/setters
Svar #8
Jag skulle vara mer benägen att göra det på det här sättet, med (valfritt) automatiska egenskaper och standardvärden. Du har inte föreslagit att de två fälten är en del av en datastruktur, så därför valde jag det här sättet.
Du skulle kunna ha egenskaperna på klassen och sedan tilldela dem på det vanliga sättet. Och uppenbarligen kan de vara obligatoriska eller inte, så det är något annat också. Det är bara det att detta är sådant trevligt syntaktiskt socker.
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
Svar #9
Du skulle kunna ha en klass med valfria fält (markerade med ?) och en konstruktor som tar emot en instans av samma klass.
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" }),
];
I det här fallet kommer du inte att kunna utelämna de obligatoriska fälten. Detta ger dig detaljerad kontroll över objektkonstruktionen.
Du kan använda konstruktorn med Partial-typen som nämnts i andra svar:
public constructor(init?:Partial<Person>) {
Object.assign(this, init);
}
Problemet är att alla fält blir valfria och det är inte önskvärt i de flesta fall.
