TypeScript e inicializadores de campo

252

Cómo iniciar una nueva clase de TStal manera (ejemplo C#para mostrar lo que quiero):

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

[editar]
Cuando escribía esta pregunta, era un desarrollador puro de .NET sin mucho conocimiento de JS. También TypeScript era algo completamente nuevo, anunciado como un nuevo superconjunto de JavaScript basado en C #. Hoy veo cuán estúpida fue esta pregunta.

De todos modos, si alguien todavía está buscando una respuesta, consulte las posibles soluciones a continuación.

Lo primero a tener en cuenta es que en TS no deberíamos crear clases vacías para los modelos. La mejor manera es crear una interfaz o tipo (según las necesidades). Buen artículo de Todd Motto: https://ultimatecourses.com/blog/classes-vs-interfaces-in-typescript

SOLUCIÓN 1:

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

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

SOLUCIÓN 2:

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

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

SOLUCIÓN 3 (cuando realmente necesitas una clase):

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

Por supuesto, en ambos casos, es posible que no necesite tipos de conversión manualmente porque se resolverán a partir del tipo de retorno de función / método.

Nickon
fuente
99
La "solución" que acaba de agregar a su pregunta no es TypeScript ni JavaScript válidos. Pero es valioso señalar que es lo más intuitivo para probar.
Jacob Foshee
1
@JacobFoshee no es válido JavaScript? Vea mis herramientas de desarrollo de Chrome: i.imgur.com/vpathu6.png (pero Visual Studio Code o cualquier otro TypeScript incluido inevitablemente se quejaría)
Mars Robertson
55
@MichalStefanow el OP editó la pregunta después de que publiqué ese comentario. Él teníareturn new MyClass { Field1: "ASD", Field2: "QWE" };
Jacob Foshee

Respuestas:

90

Actualizar

Desde que escribí esta respuesta, han surgido mejores formas. Vea las otras respuestas a continuación que tienen más votos y una mejor respuesta. No puedo eliminar esta respuesta ya que está marcada como aceptada.


Vieja respuesta

Hay un problema en el codeplex de TypeScript que describe esto: Soporte para inicializadores de objetos .

Como se indicó, ya puede hacerlo mediante el uso de interfaces en TypeScript en lugar de clases:

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
fuente
81
En su ejemplo, bob no es una instancia de la clase Persona. No veo cómo esto es equivalente al ejemplo de C #.
Jack Wester
3
los nombres de interfaz son mejores para comenzar con "I"
mayúscula
16
1. Person no es una clase y 2. @JackWester tiene razón, bob no es una instancia de Person. Probar alerta (instancia de bob de persona); En este ejemplo de código, la Persona está allí solo para fines de Afirmación de tipo.
Jacques
15
Estoy de acuerdo con Jack y Jaques, y creo que vale la pena repetirlo. Tu bob es del tipo Person , pero no es en absoluto una instancia de Person. Imagine que en Personrealidad sería una clase con un constructor complejo y un montón de métodos, este enfoque caería de bruces. Es bueno que un grupo de personas encuentre útil su enfoque, pero no es una solución a la pregunta como se dice y podría usar una persona de clase en su ejemplo en lugar de una interfaz, sería del mismo tipo.
Alex
26
¿Por qué es una respuesta aceptada cuando está claramente equivocada?
Hendrik Wiese
447

Actualizado el 12/07/2016: Typecript 2.1 presenta Tipos mapeados y proporciona Partial<T>, lo que le permite hacer esto ...

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

Respuesta original

Mi enfoque es definir una fieldsvariable separada que pase al constructor. El truco consiste en redefinir todos los campos de clase para este inicializador como opcional. Cuando se crea el objeto (con sus valores predeterminados) simplemente asigna el objeto inicializador 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 hacerlo manualmente (un poco más seguro):

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

uso:

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

y salida de consola:

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 }   

Esto le brinda seguridad básica e inicialización de la propiedad, pero todo es opcional y puede estar fuera de servicio. Si no pasa un campo, obtiene los valores predeterminados de la clase solo.

También puede mezclarlo con los parámetros de constructor requeridos: pegue fieldsal final.

Creo que lo más parecido al estilo C # que vas a conseguir ( se rechazó la sintaxis real field-init ). Prefiero el iniciador de campo apropiado, pero no parece que suceda todavía.

A modo de comparación, si utiliza el enfoque de conversión, su objeto de inicialización debe tener TODOS los campos para el tipo al que está enviando, además de no obtener ninguna función específica de clase (o derivaciones) creada por la clase misma.

Meirion Hughes
fuente
1
+1. En realidad, esto crea una instancia de una clase (que la mayoría de estas soluciones no), mantiene toda la funcionalidad dentro del constructor (sin Object.create u otro tipo de cruft fuera) y declara explícitamente el nombre de la propiedad en lugar de confiar en el orden de parámetros ( Mi preferencia personal aquí). Sin embargo, pierde seguridad de tipo / compilación entre los parámetros y las propiedades.
Violines
1
@ user1817787 probablemente sería mejor definir cualquier cosa que tenga un valor predeterminado como opcional en la clase misma, pero asigne un valor predeterminado. Entonces no use Partial<>, simplemente Person, eso requerirá que pase un objeto que tenga los campos requeridos. Dicho esto, consulte aquí las ideas (ver Selección) que limitan la asignación de tipos a ciertos campos.
Meirion Hughes
2
Esta realmente debería ser la respuesta. Es la mejor solución a este problema.
Ascherer
99
es un código de public constructor(init?:Partial<Person>) { Object.assign(this, init); }
oro
3
Si Object.assign muestra un error como lo fue para mí, vea esta respuesta SO: stackoverflow.com/a/38860354/2621693
Jim Yarbro
27

A continuación se muestra una solución que combina una aplicación más corta Object.assignpara modelar más de cerca el original.C# patrón .

Pero primero, revisemos las técnicas ofrecidas hasta ahora, que incluyen:

  1. Copie los constructores que aceptan un objeto y aplíquelo a Object.assign
  2. Un listo Partial<T> truco dentro del constructor de copias
  3. Uso de "casting" contra un POJO
  4. Apalancamiento en Object.createlugar deObject.assign

Por supuesto, cada uno tiene sus pros / contras. Modificar una clase de destino para crear un constructor de copia puede no ser siempre una opción. Y "casting" pierde las funciones asociadas con el tipo de destino. Object.createparece menos atractivo ya que requiere un mapa descriptor de propiedades bastante detallado.

La respuesta más breve de uso general

Entonces, aquí hay otro enfoque que es algo más simple, mantiene la definición de tipo y los prototipos de funciones asociadas, y modela más de cerca el C#patrón deseado :

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

Eso es. La única adición sobre el C#patrón es Object.assignjunto con 2 paréntesis y una coma. Consulte el siguiente ejemplo de trabajo para confirmar que mantiene los prototipos de función del tipo. No se requieren constructores, ni trucos ingeniosos.

Ejemplo de trabajo

Este ejemplo muestra cómo inicializar un objeto usando una aproximación de un C#inicializador de 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() );

Jonathan B.
fuente
Como este también, puede crear un objeto de usuario a partir de un objeto javascript
Dave Keane
1
6½ años después, olvidé esto, pero aún me gusta. Gracias de nuevo.
PRMan
25

Puede afectar a un objeto anónimo lanzado en su tipo de clase. Bonificación : en Visual Studio, te beneficiarás de intellisense de esta manera :)

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

Editar:

ADVERTENCIA Si la clase tiene métodos, la instancia de su clase no los obtendrá. Si AClass tiene un constructor, no se ejecutará. Si utiliza instancia de AClass, obtendrá falso.

En conclusión, debe usar la interfaz y no la clase . El uso más común es para el modelo de dominio declarado como Objetos antiguos simples. De hecho, para el modelo de dominio, debería usar mejor la interfaz en lugar de la clase. Las interfaces se utilizan en el momento de la compilación para la verificación de tipos y, a diferencia de las clases, las interfaces se eliminan por completo durante la compilación.

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

var anObject: IModel = {
     Property1: "Value",
     Property2: "Value",
     PropertyBoolean: true,
     PropertyNumber: 1
 };
rdhainaut
fuente
20
Si AClasscontuviera métodos, anInstanceno los obtendría.
Monseñor
2
Además, si AClass tiene un constructor, no se ejecutará.
Michael Erickson
1
Además, si lo haces anInstance instanceof AClass, llegarás falseen tiempo de ejecución.
Lucero
15

Sugiero un enfoque que no requiere Typecript 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();

puntos clave:

  • Mecanografiado 2.1 no requerido, Partial<T>no requerido
  • Admite funciones (en comparación con la aserción de tipo simple que no admite funciones)
VeganHunter
fuente
1
No respeta los campos obligatorios: new Person(<Person>{});(debido a la conversión) y también debe ser claro; El uso de <T> parcial admite funciones. En última instancia, si tiene campos obligatorios (más funciones de prototipo) deberá hacer lo siguiente: init: { name: string, address?: string, age: number }y descartar el elenco.
Meirion Hughes
Además, cuando obtengamos una asignación de tipo condicional , podrá asignar solo las funciones a parciales y mantener las propiedades como están. :)
Meirion Hughes
En lugar de la sofisticada classy luego var, si lo hago var dog: {name: string} = {name: 'will be assigned later'};, compila y funciona. ¿Alguna deficiencia o problema? Oh, dogtiene un alcance muy pequeño y específico, lo que significa solo una instancia.
Jeb50
11

Estaría más inclinado a hacerlo de esta manera, usando (opcionalmente) propiedades automáticas y valores predeterminados. No ha sugerido que los dos campos son parte de una estructura de datos, por eso elegí de esta manera.

Puede tener las propiedades en la clase y luego asignarlas de la manera habitual. Y, obviamente, pueden o no ser necesarios, por lo que también es otra cosa. Es solo que este es un azúcar sintáctico tan agradable.

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
fuente
66
Mis más sinceras disculpas. Pero en realidad no "preguntó sobre los inicializadores de campo", por lo que es natural suponer que podría estar interesado en formas alternativas de actualizar una clase en TS. Puede dar un poco más de información en su pregunta si está tan listo para votar negativamente.
Ralph Lavelle
El constructor +1 es el camino a seguir siempre que sea posible; pero en los casos en que tiene muchos campos y solo desea inicializar algunos de ellos, creo que mi respuesta facilita las cosas.
Meirion Hughes
1
Si tiene muchos campos, inicializar un objeto como este sería bastante difícil de manejar, ya que pasaría al constructor un muro de valores sin nombre. Eso no quiere decir que este método no tenga mérito; sería mejor usarlo para objetos simples con pocos campos. La mayoría de las publicaciones dicen que alrededor de cuatro o cinco parámetros en la firma de un miembro es el límite superior. Solo señalo esto porque encontré un enlace a esta solución en el blog de alguien mientras buscaba inicializadores TS. Tengo objetos con más de 20 campos que deben inicializarse para pruebas unitarias.
Dustin Cleveland
11

En algunos escenarios, puede ser aceptable usar Object.create . La referencia de Mozilla incluye un polyfill si necesita compatibilidad con versiones anteriores o si desea implementar su propia función de inicializador.

Aplicado a tu ejemplo:

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

Escenarios útiles

  • Pruebas unitarias
  • Declaración en línea

En mi caso, encontré esto útil en pruebas unitarias por dos razones:

  1. Cuando pruebo las expectativas, a menudo quiero crear un objeto delgado como expectativa
  2. Los marcos de prueba de unidad (como Jasmine) pueden comparar el prototipo de objeto ( __proto__) y fallar la prueba. Por ejemplo:
var actual = new MyClass();
actual.field1 = "ASD";
expect({ field1: "ASD" }).toEqual(actual); // fails

El resultado de la falla de la prueba de la unidad no dará una pista sobre lo que no coincide.

  1. En las pruebas unitarias, puedo ser selectivo sobre los navegadores que soporto

Finalmente, la solución propuesta en http://typescript.codeplex.com/workitem/334 no admite la declaración en línea de estilo json. Por ejemplo, lo siguiente no se compila:

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

Quería una solución que tuviera lo siguiente:

  • Todos los objetos de datos son obligatorios y deben ser completados por el constructor.
  • No es necesario proporcionar valores predeterminados.
  • Puede usar funciones dentro de la clase.

Aquí está la forma en que lo hago:

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

Todas las propiedades en el constructor son obligatorias y no pueden omitirse sin un error del compilador.

Depende de OnlyDataque se filtre fuera getFullName()de las propiedades requeridas y se define así:

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

Limitaciones actuales de esta manera:

  • Requiere TypeScript 2.8
  • Clases con captadores / setters
VitalyB
fuente
Esta respuesta me parece más cercana al ideal. Se pregunta si podemos hacer algo mejor con el texto mecanografiado 3+
RyanM
1
Por lo que puedo decir, esta es la mejor manera de avanzar a partir de hoy. Gracias.
Florian Norbert Bepunkt
Hay un problema con esta solución, @VitalyB: tan pronto como los métodos tienen parámetros, esto se rompe: mientras getFullName () {return "bar"} funciona, getFullName (str: string): string {return str} no funciona
florian norbert bepunkt
@floriannorbertbepunkt ¿Qué no te funciona exactamente? Parece estar funcionando bien para mí ...
rfgamaral
3

Esta es otra solución:

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

Podría tener una clase con campos opcionales (marcados con?) Y un constructor que recibe una instancia de la misma 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" }),
];

En este caso, no podrá omitir los campos obligatorios. Esto le brinda un control detallado sobre la construcción del objeto.

Puede usar el constructor con el tipo Parcial como se indica en otras respuestas:

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

El problema es que todos los campos se vuelven opcionales y no es deseable en la mayoría de los casos.

Alex
fuente
1

La forma más fácil de hacerlo es con el tipo de conversión.

return <MyClass>{ Field1: "ASD", Field2: "QWE" };
Peter Pompeya
fuente
77
Desafortunadamente, (1) no se trata de una conversión de tipo, sino de una afirmación de tipo en tiempo de compilación , (2) la pregunta fue "cómo iniciar una nueva clase " (énfasis mío), y este enfoque no logrará eso. Sería ciertamente bueno si TypeScript tuviera esta característica, pero desafortunadamente, ese no es el caso.
John Weisz
¿Dónde está la definición de MyClass?
Jeb50
0

Si está utilizando una versión anterior de mecanografiado <2.1, puede usar una similar a la siguiente, que básicamente es la conversión de cualquier objeto tipeado:

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

NOTA: El uso de este método solo es bueno para los modelos de datos, ya que eliminará todos los métodos del objeto. Básicamente es lanzar cualquier objeto a un objeto escrito

Mohsen Tabareh
fuente
-1

si desea crear una nueva instancia sin establecer el valor inicial cuando la instancia

1- tienes que usar clase no interfaz

2- tienes que establecer el valor inicial al crear la clase

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


student: IStudentDTO = new IStudentDTO();
hosam hemaily
fuente