TypeScript et initialiseurs de champ

252

Comment initier une nouvelle classe de TScette manière (exemple C#pour montrer ce que je veux):

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

[modifier]
Lorsque j'écrivais cette question, j'étais un développeur .NET pur sans beaucoup de connaissances JS. De plus, TypeScript était quelque chose de complètement nouveau, annoncé comme un nouveau surensemble de JavaScript basé sur C #. Aujourd'hui, je vois à quel point cette question était stupide.

Quoi qu'il en soit, si quelqu'un cherche toujours une réponse, veuillez consulter les solutions possibles ci-dessous.

La première chose à noter est que dans TS, nous ne devons pas créer de classes vides pour les modèles. Le meilleur moyen est de créer une interface ou un type (selon les besoins). Bon article de Todd Motto: https://ultimatecourses.com/blog/classes-vs-interfaces-in-typescript

SOLUTION 1:

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

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

SOLUTION 2:

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

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

SOLUTION 3 (quand vous avez vraiment besoin d'un cours):

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

ou

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

Bien sûr, dans les deux cas, vous n'aurez peut-être pas besoin de types de conversion manuellement car ils seront résolus à partir du type de retour de fonction / méthode.

Nickon
la source
9
La «solution» que vous venez d'ajouter à votre question n'est pas du TypeScript ou du JavaScript valide. Mais il est important de souligner que c'est la chose la plus intuitive à essayer.
Jacob Foshee
1
@JacobFoshee JavaScript non valide? Voir mes outils de développement Chrome: i.imgur.com/vpathu6.png (mais le code Visual Studio ou tout autre typeScript linted se plaindrait inévitablement)
Mars Robertson
5
@MichalStefanow l'OP a édité la question après avoir posté ce commentaire. Il avaitreturn new MyClass { Field1: "ASD", Field2: "QWE" };
Jacob Foshee

Réponses:

90

Mettre à jour

Depuis la rédaction de cette réponse, de meilleurs moyens sont apparus. Veuillez voir les autres réponses ci-dessous qui ont plus de votes et une meilleure réponse. Je ne peux pas supprimer cette réponse car elle est marquée comme acceptée.


Ancienne réponse

Il existe un problème sur le codeplex TypeScript qui décrit ceci: Prise en charge des initialiseurs d'objets .

Comme indiqué, vous pouvez déjà le faire en utilisant des interfaces dans TypeScript au lieu de classes:

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

var bob: Person = {
    name: {
        first: "Bob",
        last: "Smith",
    },
    age: 35,
};
Wouter de Kort
la source
81
Dans votre exemple, bob n'est pas une instance de la classe Person. Je ne vois pas comment cela est équivalent à l'exemple C #.
Jack Wester
3
les noms d'interface sont préférables pour commencer avec un "I" majuscule
Kiarash
16
1. Personne n'est pas une classe et 2. @JackWester a raison, bob n'est pas une instance de Personne. Essayez d'alerte (bob instanceof Person); Dans cet exemple de code, Personne n'est présent qu'à des fins d'assertion de type.
Jacques
15
Je suis d'accord avec Jack et Jaques, et je pense que cela vaut la peine d'être répété. Votre bob est du type Person , mais ce n'est pas du tout une instance de Person. Imaginez qu'en Personfait, ce serait une classe avec un constructeur complexe et un tas de méthodes, cette approche tomberait à plat sur le visage. C'est bien qu'un groupe de personnes ait trouvé votre approche utile, mais ce n'est pas une solution à la question telle qu'elle est énoncée et vous pourriez tout aussi bien utiliser une personne de classe dans votre exemple au lieu d'une interface, ce serait le même type.
Alex
26
Pourquoi est-ce une réponse acceptée alors qu'elle est clairement fausse?
Hendrik Wiese
447

Mise à jour le 12/07/2016: Typescript 2.1 présente les types mappés et fournit Partial<T>, ce qui vous permet de le faire ....

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éponse originale:

Mon approche consiste à définir une fieldsvariable distincte que vous transmettez au constructeur. L'astuce consiste à redéfinir tous les champs de classe pour cet initialiseur comme facultatifs. Lorsque l'objet est créé (avec ses valeurs par défaut), vous affectez simplement l'objet initialiseur à 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);
    }
}

ou faites-le manuellement (un peu plus sûr):

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

usage:

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

et sortie 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 }   

Cela vous donne une sécurité de base et une initialisation des propriétés, mais tout est facultatif et peut être hors service. Vous obtenez les valeurs par défaut de la classe seuls si vous ne passez pas un champ.

Vous pouvez également le mélanger avec les paramètres de constructeur requis - restez fieldssur la fin.

À peu près aussi proche du style C # que vous allez l'obtenir, je pense ( la syntaxe de l'initialisation sur le terrain a été rejetée ). Je préférerais de loin une initialisation de champ appropriée, mais cela ne semble pas encore arriver.

À titre de comparaison, si vous utilisez l'approche de transtypage, votre objet d'initialisation doit avoir TOUS les champs du type vers lequel vous transtypez, en plus de ne pas obtenir de fonctions (ou dérivations) spécifiques à la classe créées par la classe elle-même.

Meirion Hughes
la source
1
+1. Cela crée en fait une instance d'une classe (ce que la plupart de ces solutions ne font pas), conserve toutes les fonctionnalités à l'intérieur du constructeur (pas Object.create ou autre cruft à l'extérieur), et indique explicitement le nom de la propriété au lieu de s'appuyer sur l'ordre des paramètres ( ma préférence personnelle ici). Il perd cependant la sécurité de type / compilation entre les paramètres et les propriétés.
Fiddles
1
@ user1817787 vous feriez probablement mieux de définir tout ce qui a une valeur par défaut comme facultatif dans la classe elle-même, mais attribuez une valeur par défaut. Alors n'utilisez pas Partial<>, juste Person- cela vous obligera à passer un objet qui a les champs requis. cela dit, voir ici pour des idées (voir Choix) qui limitent le mappage de type à certains champs.
Meirion Hughes
2
Cela devrait vraiment être la réponse. C'est la meilleure solution à ce problème.
Ascherer
9
c'est un morceau de code public constructor(init?:Partial<Person>) { Object.assign(this, init); }
GOLD
3
Si Object.assign affiche une erreur comme c'était le cas pour moi, veuillez voir cette réponse SO: stackoverflow.com/a/38860354/2621693
Jim Yarbro
27

Vous trouverez ci-dessous une solution qui combine une application plus courte de Object.assignpour modéliser plus étroitement le C#motif d' origine .

Mais d'abord, passons en revue les techniques proposées jusqu'à présent, qui comprennent:

  1. Copiez les constructeurs qui acceptent un objet et appliquez-le à Object.assign
  2. Une Partial<T>astuce astucieuse dans le constructeur de copie
  3. Utilisation de "casting" contre un POJO
  4. Tirer parti Object.createau lieu deObject.assign

Bien sûr, chacun a ses avantages / inconvénients. La modification d'une classe cible pour créer un constructeur de copie n'est pas toujours une option. Et le «casting» perd toutes les fonctions associées au type cible. Object.createsemble moins attrayant car il nécessite une carte de descripteur de propriété assez verbeuse.

Réponse la plus courte et polyvalente

Voici donc une autre approche quelque peu plus simple, qui maintient la définition de type et les prototypes de fonctions associés, et modélise plus étroitement le C#modèle souhaité :

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

C'est tout. Le seul ajout sur leC# motif est Object.assignaccompagné de 2 parenthèses et d'une virgule. Consultez l'exemple de travail ci-dessous pour confirmer qu'il conserve les prototypes de fonction du type. Aucun constructeur requis et aucune astuce intelligente.

Exemple de travail

Cet exemple montre comment initialiser un objet à l'aide d'une approximation d'un C#initialiseur de champ:

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

Jonathan B.
la source
Comme celui-ci aussi, peut créer un objet utilisateur à partir d'un objet javascript
Dave Keane
1
6 ans et demi plus tard, j'ai oublié cela, mais je l'aime toujours. Merci encore.
PRMan
25

Vous pouvez affecter un objet anonyme casté dans votre type de classe. Bonus : Dans Visual Studio, vous bénéficiez de l'intellisense de cette façon :)

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

Éditer:

AVERTISSEMENT Si la classe a des méthodes, l'instance de votre classe ne les obtiendra pas. Si AClass a un constructeur, il ne sera pas exécuté. Si vous utilisez instanceof AClass, vous obtiendrez faux.

En conclusion, vous devez utiliser l'interface et non la classe . L'utilisation la plus courante concerne le modèle de domaine déclaré comme Plain Old Objects. En effet, pour le modèle de domaine, il vaut mieux utiliser l'interface plutôt que la classe. Les interfaces sont utilisées au moment de la compilation pour la vérification de type et contrairement aux classes, les interfaces sont complètement supprimées pendant la compilation.

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

var anObject: IModel = {
     Property1: "Value",
     Property2: "Value",
     PropertyBoolean: true,
     PropertyNumber: 1
 };
rdhainaut
la source
20
Si elles AClasscontenaient des méthodes, anInstanceje ne les obtiendrais pas.
Mgr
2
De plus, si AClass a un constructeur, il ne sera pas exécuté.
Michael Erickson
1
De plus, si vous le faites, anInstance instanceof AClassvous obtiendrez falseau moment de l'exécution.
Lucero
15

Je suggère une approche qui ne nécessite pas 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();

points clés:

  • Typographie 2.1 non requise, Partial<T>non requise
  • Il prend en charge les fonctions (en comparaison avec l'assertion de type simple qui ne prend pas en charge les fonctions)
VeganHunter
la source
1
Il ne respecte pas les champs obligatoires: new Person(<Person>{});(car casting) et pour être clair aussi; l'utilisation de <T> partiel prend en charge les fonctions. En fin de compte, si vous avez des champs obligatoires (plus des fonctions de prototype), vous devrez faire: init: { name: string, address?: string, age: number }et supprimer le casting.
Meirion Hughes
De plus, lorsque nous obtenons un mappage de type conditionnel, vous pourrez mapper uniquement les fonctions aux partiels et conserver les propriétés telles quelles. :)
Meirion Hughes
Au lieu du sophistiqué classalors var, si je le fais var dog: {name: string} = {name: 'will be assigned later'};, il compile et fonctionne. Une déficience ou un problème? Oh, doga une très petite portée et spécifique, ce qui signifie qu'une seule instance.
Jeb50
11

Je serais plus enclin à le faire de cette façon, en utilisant (éventuellement) des propriétés et des valeurs par défaut automatiques. Vous n'avez pas suggéré que les deux champs font partie d'une structure de données, c'est pourquoi j'ai choisi cette façon.

Vous pouvez avoir les propriétés de la classe, puis les affecter de la manière habituelle. Et évidemment, ils peuvent ou non être nécessaires, c'est donc autre chose aussi. C'est juste que c'est un si bon sucre syntaxique.

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
Ralph Lavelle
la source
6
Mes plus sincères excuses. Mais vous n'avez en fait pas "posé de questions sur les initialiseurs de champ", il est donc naturel de supposer que vous pourriez être intéressé par d'autres façons de renouveler une classe dans TS. Vous pourriez donner un tout petit peu plus d'informations dans votre question si vous êtes prêt à voter.
Ralph Lavelle
+1 constructeur est la voie à suivre lorsque cela est possible; mais dans les cas où vous avez beaucoup de champs et que vous voulez seulement en initialiser certains, je pense que ma réponse facilite les choses.
Meirion Hughes
1
Si vous avez plusieurs champs, initialiser un objet comme celui-ci serait assez compliqué car vous passeriez au constructeur un mur de valeurs sans nom. Cela ne veut pas dire que cette méthode n'a aucun mérite; il serait préférable de l'utiliser pour des objets simples avec peu de champs. La plupart des publications disent qu'environ quatre ou cinq paramètres dans la signature d'un membre est la limite supérieure. Je le souligne simplement parce que j'ai trouvé un lien vers cette solution sur le blog de quelqu'un lors de la recherche des initialiseurs TS. J'ai des objets avec plus de 20 champs qui doivent être initialisés pour des tests unitaires.
Dustin Cleveland
11

Dans certains scénarios, il peut être acceptable d'utiliser Object.create. La référence Mozilla inclut un polyfill si vous avez besoin d'une rétrocompatibilité ou si vous souhaitez utiliser votre propre fonction d'initialisation.

Appliqué à votre exemple:

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

Scénarios utiles

  • Tests unitaires
  • Déclaration en ligne

Dans mon cas, j'ai trouvé cela utile dans les tests unitaires pour deux raisons:

  1. Lorsque je teste des attentes, je veux souvent créer un objet mince comme une attente
  2. Les frameworks de tests unitaires (comme Jasmine) peuvent comparer le prototype d'objet ( __proto__) et échouer au test. Par exemple:
var actual = new MyClass();
actual.field1 = "ASD";
expect({ field1: "ASD" }).toEqual(actual); // fails

La sortie de l'échec du test unitaire ne donnera aucun indice sur ce qui ne correspond pas.

  1. Dans les tests unitaires, je peux être sélectif sur les navigateurs que je supporte

Enfin, la solution proposée sur http://typescript.codeplex.com/workitem/334 ne prend pas en charge la déclaration de style json en ligne. Par exemple, ce qui suit ne se compile pas:

var o = { 
  m: MyClass: { Field1:"ASD" }
};
Jacob Foshee
la source
9

Je voulais une solution qui aurait les éléments suivants:

  • Tous les objets de données sont obligatoires et doivent être remplis par le constructeur.
  • Pas besoin de fournir des valeurs par défaut.
  • Peut utiliser des fonctions à l'intérieur de la classe.

Voici comment je le fais:

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

Toutes les propriétés du constructeur sont obligatoires et ne peuvent pas être omises sans une erreur de compilation.

Il dépend de celui OnlyDataqui filtre getFullName()les propriétés requises et il est défini comme suit:

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

Limitations actuelles de cette manière:

  • Nécessite TypeScript 2.8
  • Cours avec getters / setters
VitalyB
la source
Cette réponse me semble la plus proche de l'idéal. Vous vous demandez si nous pouvons faire mieux avec le tapuscrit 3+
RyanM
1
Pour autant que je sache, c'est la meilleure façon d'avancer à ce jour. Je vous remercie.
florian norbert bepunkt le
Il y a un problème avec cette solution, @VitalyB: Dès que les méthodes ont des paramètres, cela casse: Alors que getFullName () {return "bar"} fonctionne, getFullName (str: string): string {return str} ne fonctionne pas
florian norbert bepunkt
@floriannorbertbepunkt Qu'est-ce qui ne fonctionne pas exactement pour vous? Cela semble bien fonctionner pour moi ...
rfgamaral
3

Voici une autre solution:

return {
  Field1 : "ASD",
  Field2 : "QWE" 
} as myClass;
rostamiani
la source
2

Vous pouvez avoir une classe avec des champs facultatifs (marqués avec?) Et un constructeur qui reçoit une instance de la même 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" }),
];

Dans ce cas, vous ne pourrez pas omettre les champs obligatoires. Cela vous donne un contrôle fin sur la construction de l'objet.

Vous pouvez utiliser le constructeur avec le type Partiel comme indiqué dans d'autres réponses:

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

Le problème est que tous les champs deviennent facultatifs et ce n'est pas souhaitable dans la plupart des cas.

Alex
la source
1

La façon la plus simple de procéder consiste à effectuer une conversion de type.

return <MyClass>{ Field1: "ASD", Field2: "QWE" };
Peter Pompeii
la source
7
Malheureusement, (1) il ne s'agit pas d'une conversion de type, mais d'une assertion de type au moment de la compilation , (2) la question demandait "comment lancer une nouvelle classe " (c'est moi qui souligne), et cette approche échouera. Ce serait certainement bien si TypeScript avait cette fonctionnalité, mais malheureusement, ce n'est pas le cas.
John Weisz
où est la définition de MyClass?
Jeb50
0

Si vous utilisez une ancienne version de typescript <2.1, vous pouvez utiliser une méthode similaire à la suivante, qui consiste à convertir n'importe quel objet en objet tapé:

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

REMARQUE: l'utilisation de cette méthode n'est valable que pour les modèles de données car elle supprimera toutes les méthodes de l'objet. Il s'agit essentiellement de convertir n'importe quel objet en objet tapé

Mohsen Tabareh
la source
-1

si vous souhaitez créer une nouvelle instance sans définir la valeur initiale lors de l'instance

1- vous devez utiliser la classe et non l'interface

2- vous devez définir la valeur initiale lors de la création de la classe

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


student: IStudentDTO = new IStudentDTO();
hosam hemaily
la source