TypeScript e inizializzatori di campo


252

Come avviare una nuova classe in TSmodo tale (esempio in C#per mostrare ciò che voglio):

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

[modifica]
Quando stavo scrivendo questa domanda, ero un puro sviluppatore .NET senza molta conoscenza di JS. Anche TypeScript era qualcosa di completamente nuovo, annunciato come nuovo superset di JavaScript basato su C #. Oggi vedo quanto sia stata stupida questa domanda.

Comunque, se qualcuno è ancora alla ricerca di una risposta, per favore, guarda le possibili soluzioni di seguito.

La prima cosa da notare è che in TS non dovremmo creare classi vuote per i modelli. Il modo migliore è creare un'interfaccia o un tipo (a seconda delle esigenze). Buon articolo da Todd Motto: https://ultimatecourses.com/blog/classes-vs-interfaces-in-typescript

SOLUZIONE 1:

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

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

SOLUZIONE 2:

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

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

SOLUZIONE 3 (quando hai davvero bisogno di una lezione):

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

o

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

Ovviamente in entrambi i casi potrebbe non essere necessario eseguire manualmente i tipi di cast perché verranno risolti dal tipo restituito da funzione / metodo.


9
La "soluzione" che hai appena aggiunto alla tua domanda non è TypeScript o JavaScript validi. Ma è utile sottolineare che è la cosa più intuitiva da provare.
Jacob Foshee,

1
@JacobFoshee non è valido JavaScript? Vedi i miei Chrome Dev Tools: i.imgur.com/vpathu6.png (ma il codice di Visual Studio o qualsiasi altro tipo di codice in codice potrebbe inevitabilmente lamentarsi)
Mars Robertson,

5
@MichalStefanow l'OP ha modificato la domanda dopo aver pubblicato quel commento. Avevareturn new MyClass { Field1: "ASD", Field2: "QWE" };
Jacob Foshee il

Risposte:


90

Aggiornare

Da quando ho scritto questa risposta, sono emersi modi migliori. Si prega di vedere le altre risposte di seguito che hanno più voti e una risposta migliore. Non riesco a rimuovere questa risposta poiché è contrassegnata come accettata.


Vecchia risposta

C'è un problema nel codeplex TypeScript che descrive questo: Supporto per inizializzatori di oggetti .

Come detto, puoi già farlo utilizzando le interfacce in TypeScript anziché le classi:

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

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

81
Nel tuo esempio, bob non è un'istanza di classe Person. Non vedo come questo sia equivalente all'esempio C #.
Jack Wester,

3
è meglio iniziare i nomi delle interfacce con la "I"
maiuscola

16
1. Person non è una classe e 2. @JackWester ha ragione, Bob non è un'istanza di Person. Prova avviso (bob instanceof Person); In questo esempio di codice Person è presente solo a scopo di asserzione di tipo.
Jacques,

15
Sono d'accordo con Jack e Jaques e penso che valga la pena ripeterlo. Il tuo bob è del tipo Person , ma non è affatto un'istanza di Person. Immagina che in Personrealtà sarebbe una classe con un costruttore complesso e un mucchio di metodi, questo approccio sarebbe piatto. È positivo che un gruppo di persone abbia trovato utile il tuo approccio, ma non è una soluzione alla domanda come è stato affermato e potresti anche usare una persona di classe nel tuo esempio anziché un'interfaccia, sarebbe dello stesso tipo.
Alex

26
Perché questa è una risposta accettata quando è chiaramente sbagliata?
Hendrik Wiese,

447

Aggiornato il 07/12/2016: Typescript 2.1 introduce i tipi mappati e fornisce Partial<T>, che ti consente di farlo ....

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

Risposta originale:

Il mio approccio è definire una fieldsvariabile separata che passi al costruttore. Il trucco è ridefinire tutti i campi di classe per questo inizializzatore come facoltativo. Quando viene creato l'oggetto (con i suoi valori predefiniti) è sufficiente assegnare l'oggetto inizializzatore su 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);
    }
}

o fallo manualmente (un po 'più sicuro):

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

utilizzo:

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

e uscita console:

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 }   

Questo ti dà la sicurezza di base e l'inizializzazione della proprietà, ma è tutto opzionale e può essere fuori servizio. Le impostazioni predefinite della classe vengono lasciate sole se non si passa un campo.

Puoi anche mischiarlo con i parametri costruttori richiesti - attacca fieldsalla fine.

Penso che sia il più vicino allo stile C # che hai intenzione di pensare ( la sintassi del field-init reale è stata respinta ). Preferirei di gran lunga l'inizializzatore di campo corretto, ma non sembra che accadrà ancora.

Per fare un confronto, se usi l'approccio di casting, il tuo oggetto inizializzatore deve avere TUTTI i campi per il tipo in cui stai lanciando, inoltre non ottenere alcuna funzione (o derivazione) specifica della classe creata dalla classe stessa.


1
+1. Questo in realtà crea un'istanza di una classe (che la maggior parte di queste soluzioni non ha), mantiene tutte le funzionalità all'interno del costruttore (nessun Object.create o altra cruft all'esterno) e afferma esplicitamente il nome della proprietà invece di fare affidamento su param ordering ( le mie preferenze personali qui). Tuttavia, perde la sicurezza di tipo / compilazione tra parametri e proprietà.
Fiddles,

1
@utente1817787 probabilmente staresti meglio nel definire qualsiasi cosa abbia un valore predefinito come facoltativo nella classe stessa, ma assegna un valore predefinito. Quindi non usare Partial<>, solo Person- ciò richiederà di passare un oggetto che ha i campi richiesti. detto questo, vedi qui per idee (Vedi Pick) che limitano la mappatura dei tipi a determinati campi.
Meirion Hughes,

2
Questa dovrebbe davvero essere la risposta. È la migliore soluzione a questo problema.
Ascherer,

9
questo è un pezzo di codice public constructor(init?:Partial<Person>) { Object.assign(this, init); }
DORATO

3
Se Object.assign mostra un errore come è stato per me, vedere questa risposta SO: stackoverflow.com/a/38860354/2621693
Jim Yarbro,

27

Di seguito è una soluzione che combina un'applicazione più breve Object.assignper modellare più da vicino il modello originale C#.

Ma prima, esaminiamo le tecniche offerte finora, che includono:

  1. Copia i costruttori che accettano un oggetto e applicalo a Object.assign
  2. Un Partial<T>trucco intelligente nel costruttore di copie
  3. Uso del "casting" contro un POJO
  4. Sfruttando Object.createinvece diObject.assign

Naturalmente, ognuno ha i suoi pro / contro. La modifica di una classe di destinazione per creare un costruttore di copie potrebbe non essere sempre un'opzione. E il "casting" perde tutte le funzioni associate al tipo di destinazione. Object.createsembra meno attraente poiché richiede una mappa descrittore di proprietà piuttosto dettagliata.

Risposta più breve e generica

Quindi, ecco ancora un altro approccio che è un po 'più semplice, mantiene la definizione del tipo e i prototipi di funzione associati e modella più da vicino il C#modello previsto :

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

Questo è tutto. L'unica aggiunta al C#modello è Object.assigninsieme a 2 parentesi e una virgola. Dai un'occhiata all'esempio di lavoro che segue per confermare che mantiene i prototipi di funzione del tipo. Non sono richiesti costruttori e nessun trucco intelligente.

Esempio di lavoro

Questo esempio mostra come inizializzare un oggetto usando un'approssimazione di un C#inizializzatore di campo:

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


Come questo, puoi creare un oggetto utente da un oggetto javascript
Dave Keane,

1
6 anni e mezzo dopo l'ho dimenticato, ma mi piace ancora. Grazie ancora.
PRMan,

25

Puoi influenzare un oggetto anonimo lanciato nel tuo tipo di classe. Bonus : in Visual Studio, beneficerai dell'intellisense in questo modo :)

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

Modificare:

AVVERTIMENTO Se la classe ha dei metodi, l'istanza della tua classe non li otterrà. Se AClass ha un costruttore, non verrà eseguito. Se usi l'istanza di AClass, otterrai false.

In conclusione, dovresti usare l'interfaccia e non la classe . L'uso più comune è per il modello di dominio dichiarato come vecchi oggetti semplici. In effetti, per il modello di dominio dovresti usare meglio l'interfaccia invece della classe. Le interfacce vengono utilizzate al momento della compilazione per il controllo del tipo e, diversamente dalle classi, le interfacce vengono completamente rimosse durante la compilazione.

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

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

20
Se i AClassmetodi contenuti, anInstancenon li ottengono.
Mons.

2
Inoltre, se AClass ha un costruttore, non verrà eseguito.
Michael Erickson,

1
Inoltre, se lo fai anInstance instanceof AClassotterrai falsein fase di esecuzione.
Lucero,

15

Suggerisco un approccio che non richiede 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();

punti chiave:

  • Dattiloscritto 2.1 non richiesto, Partial<T>non richiesto
  • Supporta funzioni (rispetto a un'asserzione di tipo semplice che non supporta funzioni)

1
Non rispetta i campi obbligatori: new Person(<Person>{});(perché casting) e anche per essere chiari; l'utilizzo di <T> parziale supporta le funzioni. Alla fine, se hai campi obbligatori (più funzioni prototipo) dovrai fare: init: { name: string, address?: string, age: number }e rilasciare il cast.
Meirion Hughes,

Inoltre, quando otteniamo la mappatura del tipo condizionale , sarai in grado di mappare solo le funzioni ai parziali e mantenere le proprietà così come sono. :)
Meirion Hughes

Invece del sofisticato , classquindi var, se lo faccio var dog: {name: string} = {name: 'will be assigned later'};, si compila e funziona. Qualche carenza o problema? Oh, dogha un ambito molto piccolo e specifico, il che significa solo un'istanza.
Jeb50

11

Sarei più propenso a farlo in questo modo, usando (facoltativamente) proprietà e valori predefiniti automatici. Non hai suggerito che i due campi facciano parte di una struttura di dati, quindi è per questo che ho scelto in questo modo.

Potresti avere le proprietà sulla classe e quindi assegnarle nel solito modo. E ovviamente potrebbero essere richiesti o meno, quindi anche questo è qualcos'altro. È solo che questo è così bello zucchero sintattico.

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
Le mie scuse più profonde. Ma in realtà non hai "chiesto informazioni sugli inizializzatori di campo", quindi è naturale presumere che potresti essere interessato a modi alternativi di rinnovare una classe in TS. Potresti dare un po 'più di informazioni alla tua domanda se sei così pronto a votare.
Ralph Lavelle,

Il costruttore +1 è la strada da percorrere ove possibile; ma nei casi in cui hai molti campi e vuoi inizializzarne solo alcuni, penso che la mia risposta renda le cose più facili.
Meirion Hughes,

1
Se hai molti campi, inizializzare un oggetto come questo sarebbe piuttosto ingombrante in quanto passeresti al costruttore un muro di valori senza nome. Ciò non significa che questo metodo non abbia alcun merito; sarebbe meglio usato per oggetti semplici con pochi campi. La maggior parte delle pubblicazioni afferma che il limite superiore è rappresentato da circa quattro o cinque parametri nella firma di un membro. Lo sto solo sottolineando perché ho trovato un link a questa soluzione sul blog di qualcuno durante la ricerca di inizializzatori TS. Ho oggetti con oltre 20 campi che devono essere inizializzati per unit test.
Dustin Cleveland,

11

In alcuni scenari potrebbe essere accettabile l'utilizzo Object.create. Il riferimento Mozilla include un polyfill se è necessaria la retrocompatibilità o si desidera eseguire il rollback della propria funzione di inizializzazione.

Applicato al tuo esempio:

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

Scenari utili

  • Test unitari
  • Dichiarazione in linea

Nel mio caso l'ho trovato utile nei test unitari per due motivi:

  1. Quando collaudo le aspettative, desidero spesso creare un oggetto sottile come aspettativa
  2. I framework di unit test (come Jasmine) possono confrontare l'oggetto prototipo ( __proto__) e fallire il test. Per esempio:
var actual = new MyClass();
actual.field1 = "ASD";
expect({ field1: "ASD" }).toEqual(actual); // fails

L'output del fallimento del test unitario non fornirà un indizio su ciò che non corrisponde.

  1. Nei test unitari posso essere selettivo su quali browser io supporti

Infine, la soluzione proposta su http://typescript.codeplex.com/workitem/334 non supporta la dichiarazione in stile json in linea. Ad esempio, non viene compilato quanto segue:

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

9

Volevo una soluzione che avrebbe avuto il seguente:

  • Tutti gli oggetti dati sono richiesti e devono essere compilati dal costruttore.
  • Non è necessario fornire i valori predefiniti.
  • Può usare le funzioni all'interno della classe.

Ecco come lo faccio:

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

Tutte le proprietà nel costruttore sono obbligatorie e non possono essere omesse senza un errore del compilatore.

Dipende da OnlyDataciò che filtra fuori getFullName()dalle proprietà richieste ed è definito in questo modo:

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

Limitazioni attuali di questo modo:

  • Richiede TypeScript 2.8
  • Classi con getter / setter

Questa risposta mi sembra la più vicina all'ideale.
Mi chiedo

1
Per quanto ne so, questo è il modo migliore di procedere da oggi. Grazie.
Florian Norbert Bepunkt,

C'è un problema con questa soluzione, @VitalyB: non appena i metodi hanno parametri, questo si rompe: Mentre getFullName () {return "bar"} funziona, getFullName (str: string): string {return str} non funziona
Florian Norbert Bepunkt,

@floriannorbertbepunkt Cosa non funziona esattamente per te? Sembra funzionare bene per me ...
rfgamaral

3

Questa è un'altra soluzione:

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

2

Potresti avere una classe con campi opzionali (contrassegnati con?) E un costruttore che riceve un'istanza della stessa classe.

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 questo caso, non sarai in grado di omettere i campi richiesti. Questo ti dà un controllo accurato sulla costruzione dell'oggetto.

È possibile utilizzare il costruttore con il tipo Parziale come indicato nelle altre risposte:

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

Il problema è che tutti i campi diventano opzionali e non è auspicabile nella maggior parte dei casi.


1

Il modo più semplice per farlo è con il tipo casting.

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

7
Sfortunatamente, (1) questo non è un tipo-casting, ma un'asserzione di tipo in fase di compilazione , (2) la domanda si poneva "come iniziare una nuova classe " (enfatizza la mia), e questo approccio non riuscirà a realizzarlo. Sarebbe sicuramente bello se TypeScript avesse questa funzionalità, ma sfortunatamente non è così.
John Weisz,

dov'è la definizione di MyClass?
Jeb50,

0

Se stai usando una vecchia versione di dattiloscritto <2.1 allora puoi usare qualcosa di simile al seguente che fondamentalmente sta lanciando qualsiasi oggetto tipizzato:

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

NOTA: l'utilizzo di questo metodo è valido solo per i modelli di dati in quanto rimuoverà tutti i metodi nell'oggetto. Fondamentalmente sta lanciando qualsiasi oggetto su un oggetto tipizzato


-1

se si desidera creare una nuova istanza senza impostare il valore iniziale quando istanza

1- devi usare la classe non l'interfaccia

2- devi impostare il valore iniziale quando crei la classe

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


student: IStudentDTO = new IStudentDTO();
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.