TypeScript e inicializadores de campo

252

Como iniciar uma nova classe TSdessa maneira (exemplo C#para mostrar o que eu quero):

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

[edit]
Quando eu estava escrevendo esta pergunta, eu era desenvolvedor .NET puro, sem muito conhecimento de JS. Além disso, o TypeScript era algo completamente novo, anunciado como um novo superconjunto de JavaScript baseado em C #. Hoje vejo como essa pergunta foi estúpida.

De qualquer forma, se alguém ainda estiver procurando por uma resposta, observe as possíveis soluções abaixo.

A primeira coisa a observar é que no TS não devemos criar classes vazias para modelos. A melhor maneira é criar interface ou tipo (dependendo das necessidades). Bom artigo de Todd Motto: https://ultimatecourses.com/blog/classes-vs-interfaces-in-typescript

SOLUÇÃO 1:

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

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

SOLUÇÃO 2:

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

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

SOLUÇÃO 3 (quando você realmente precisa de uma aula):

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

Obviamente, nos dois casos, você pode não precisar de tipos de conversão manualmente, porque eles serão resolvidos a partir do tipo de retorno de função / método.

Nickon
fonte
9
A "solução" que você acabou de anexar à sua pergunta não é TypeScript ou JavaScript válido. Mas é valioso ressaltar que é a coisa mais intuitiva a se tentar.
12139 Jacob Foshee
1
@JacobFoshee não é JavaScript válido? Ver meus Ferramentas Chrome Dev: i.imgur.com/vpathu6.png (mas Visual Código Studio ou qualquer outro texto datilografado linted inevitavelmente reclamar)
Mars Robertson
5
@MichalStefanow o OP editou a pergunta depois que eu publiquei esse comentário. Ele tinhareturn new MyClass { Field1: "ASD", Field2: "QWE" };
Jacob Foshee

Respostas:

90

Atualizar

Desde que escrevemos esta resposta, surgiram maneiras melhores. Por favor, veja as outras respostas abaixo que têm mais votos e uma resposta melhor. Não consigo remover esta resposta, pois está marcada como aceita.


Resposta antiga

Há um problema no codeplex do TypeScript que descreve isso: Suporte para inicializadores de objetos .

Como afirmado, você já pode fazer isso usando interfaces no TypeScript em vez 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
fonte
81
No seu exemplo, bob não é uma instância da classe Person. Não vejo como isso é equivalente ao exemplo de c #.
23713 Jack Wester
3
nomes de interface são melhor para ser iniciado com "I" maiúsculo
Kiarash
16
1. Person não é uma classe e 2. @ JackWester está certo, bob não é uma instância de Person. Tente alerta (instância do bob de Person); Neste exemplo de código, Person está lá apenas para fins de asserção de tipo.
Jacques
15
Eu concordo com Jack e Jaques, e acho que vale a pena repetir. Seu bob é do tipo Person , mas não é uma instância de Person. Imagine que, Personna verdade, seria uma classe com um construtor complexo e um monte de métodos, essa abordagem seria totalmente irrelevante. É bom que muitas pessoas achem sua abordagem útil, mas não é uma solução para a questão, como foi afirmado, e você poderia usar uma classe Person no seu exemplo, em vez de uma interface, seria do mesmo tipo.
Alex4 #
26
Por que essa é uma resposta aceita quando está claramente errada?
Hendrik Wiese
447

Atualizado em 12/07/2016: o Typecript 2.1 apresenta os tipos mapeados e fornece Partial<T>, o que permite fazer isso ....

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

Resposta original:

Minha abordagem é definir uma fieldsvariável separada que você passa para o construtor. O truque é redefinir todos os campos de classe para este inicializador como opcional. Quando o objeto é criado (com seus padrões), você simplesmente atribui o 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);
    }
}

ou faça manualmente (um pouco mais 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
]; 

e saída do 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 }   

Isso fornece segurança básica e inicialização de propriedade, mas tudo é opcional e pode estar com defeito. Você fica com os padrões da classe em paz se não passa um campo.

Você também pode misturá-lo com os parâmetros de construtor necessários - fique fieldsno final.

Eu acho que o mais próximo do estilo C # ( a sintaxe real de init de campo foi rejeitada ). Eu preferiria o inicializador de campo adequado, mas não parece que isso ainda aconteça.

Para comparação, se você usar a abordagem de conversão, seu objeto inicializador deverá ter TODOS os campos para o tipo para o qual você está convertendo, além de não obter nenhuma função específica da classe (ou derivação) criada pela própria classe.

Meirion Hughes
fonte
1
+1. Na verdade, isso cria uma instância de uma classe (que a maioria dessas soluções não faz), mantém toda a funcionalidade dentro do construtor (nenhum Object.create ou outro cruft fora) e declara explicitamente o nome da propriedade em vez de depender da ordem dos parâmetros ( minha preferência pessoal aqui). Porém, ele perde a segurança de tipo / compilação entre os parâmetros e as propriedades.
Fiddles
1
@ user1817787 provavelmente seria melhor definir qualquer coisa que tenha um padrão como opcional na própria classe, mas atribua um padrão. Então não use Partial<>, apenas Person- isso exigirá que você passe um objeto que possui os campos obrigatórios. dito isso, consulte aqui as idéias (consulte Seleção) que limitam o mapeamento de tipos a determinados campos.
Meirion Hughes
2
Esta realmente deve ser a resposta. É a melhor solução para esse problema.
Ascherer
9
que é um pedaço de código GOLDENpublic constructor(init?:Partial<Person>) { Object.assign(this, init); }
kuncevic.dev
3
Se Object.assign está mostrando um erro como foi para mim, por favor, veja esta resposta SO: stackoverflow.com/a/38860354/2621693
Jim Yarbro
27

Abaixo está uma solução que combina uma aplicação mais curta Object.assignpara modelar mais de perto o C#padrão original .

Mas primeiro, vamos revisar as técnicas oferecidas até o momento, que incluem:

  1. Copie os construtores que aceitam um objeto e o aplicam a Object.assign
  2. Um Partial<T>truque inteligente dentro do construtor de cópias
  3. Uso de "casting" contra um POJO
  4. Aproveitando em Object.createvez deObject.assign

Obviamente, cada um tem seus prós / contras. Modificar uma classe de destino para criar um construtor de cópias nem sempre pode ser uma opção. E "vazamento" perde todas as funções associadas ao tipo de destino. Object.createparece menos atraente, pois requer um mapa descritor de propriedades bastante detalhado.

Resposta mais curta, de uso geral

Portanto, aqui está outra abordagem que é um pouco mais simples, mantém a definição de tipo e os protótipos de funções associadas e modela mais de perto o C#padrão pretendido :

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

É isso aí. A única adição sobre o C#padrão é Object.assignjuntamente com 2 parênteses e vírgula. Confira o exemplo de trabalho abaixo para confirmar que mantém os protótipos de função do tipo. Não são necessários construtores nem truques inteligentes.

Exemplo de trabalho

Este exemplo mostra como inicializar um objeto usando uma aproximação de um 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.
fonte
Como este também pode criar um objeto de usuário a partir de um objeto javascript
Dave Keane
1
6 anos e meio depois eu esqueci isso, mas ainda gosto. Obrigado novamente.
PRMan
25

Você pode afetar um objeto anônimo convertido no seu tipo de classe. Bônus : No visual studio, você se beneficia do intellisense dessa maneira :)

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

Editar:

AVISO Se a classe tiver métodos, a instância da sua classe não os obterá. Se o AClass tiver um construtor, ele não será executado. Se você usar instanceof AClass, ficará falso.

Em conclusão, você deve usar a interface e não a classe . O uso mais comum é para o modelo de domínio declarado como Objetos Antigos Simples. De fato, para o modelo de domínio, você deve usar melhor a interface em vez da classe. As interfaces são usadas no momento da compilação para verificação de tipo e, diferentemente das classes, as interfaces são completamente removidas durante a compilação.

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

var anObject: IModel = {
     Property1: "Value",
     Property2: "Value",
     PropertyBoolean: true,
     PropertyNumber: 1
 };
rdhainaut
fonte
20
Se AClasscontivesse métodos, anInstancenão os obteria.
Monsenhor
2
Além disso, se o AClass tiver um construtor, ele não será executado.
Michael Erickson
1
Além disso, se o fizer, anInstance instanceof AClassvocê obterá falseem tempo de execução.
Lucero
15

Sugiro uma abordagem que não requer o 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();

pontos chave:

  • Texto datilografado 2.1 não obrigatório, Partial<T>não obrigatório
  • Ele suporta funções (em comparação com asserção de tipo simples que não suporta funções)
VeganHunter
fonte
1
Ele não respeita os campos obrigatórios: new Person(<Person>{});(porque a transmissão) e para ser claro também; usar Parcial <T> suporta funções. Por fim, se você tiver campos obrigatórios (mais funções de protótipo), precisará fazer: init: { name: string, address?: string, age: number }e largar o elenco.
Meirion Hughes
Além disso, quando obtivermos o mapeamento de tipo condicional, você poderá mapear apenas as funções para parciais e manter as propriedades como estão. :)
Meirion Hughes
Em vez do sofisticado classentão var, se eu fizer var dog: {name: string} = {name: 'will be assigned later'};, ele compila e obras. Alguma deficiência ou problema? Oh, dogtem escopo muito pequeno e específico, significando apenas uma instância.
Jeb50
11

Eu estaria mais inclinado a fazê-lo dessa maneira, usando (opcionalmente) propriedades e padrões automáticos. Você não sugeriu que os dois campos façam parte de uma estrutura de dados, por isso escolhi esse caminho.

Você pode ter as propriedades da classe e atribuí-las da maneira usual. E, obviamente, eles podem ou não ser necessários, então isso também é outra coisa. É que esse açúcar sintático é tão bom.

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
fonte
6
Minhas desculpas mais profundas. Mas na verdade você não "perguntou sobre inicializadores de campo", por isso é natural supor que você possa estar interessado em maneiras alternativas de criar uma classe no TS. Você pode fornecer um pouco mais de informação em sua pergunta, se estiver pronto para votar.
22813 Ralph Lavelle
O construtor +1 é o caminho a seguir, sempre que possível; mas nos casos em que você tem muitos campos e deseja inicializar apenas alguns deles, acho que minha resposta facilita as coisas.
Meirion Hughes
1
Se você tiver muitos campos, inicializar um objeto como esse seria bastante complicado, pois você passaria ao construtor uma parede de valores sem nome. Isso não quer dizer que esse método não tenha mérito; seria melhor usado para objetos simples com poucos campos. A maioria das publicações diz que cerca de quatro ou cinco parâmetros na assinatura de um membro é o limite superior. Apenas apontando isso porque encontrei um link para esta solução no blog de alguém enquanto procurava inicializadores TS. Eu tenho objetos com mais de 20 campos que precisam ser inicializados para testes de unidade.
Dustin Cleveland
11

Em alguns cenários, pode ser aceitável usar Object.create. A referência do Mozilla inclui um polyfill se você precisar de retrocompatibilidade ou desejar rolar sua própria função de inicializador.

Aplicado ao seu exemplo:

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

Cenários úteis

  • Testes unitários
  • Declaração em linha

No meu caso, achei isso útil em testes de unidade por dois motivos:

  1. Ao testar as expectativas, muitas vezes quero criar um objeto fino como uma expectativa
  2. As estruturas de teste de unidade (como o Jasmine) podem comparar o protótipo do objeto ( __proto__) e falhar no teste. Por exemplo:
var actual = new MyClass();
actual.field1 = "ASD";
expect({ field1: "ASD" }).toEqual(actual); // fails

A saída da falha no teste de unidade não dará uma pista sobre o que é incompatível.

  1. Nos testes de unidade, posso ser seletivo em relação aos navegadores suportados

Por fim, a solução proposta em http://typescript.codeplex.com/workitem/334 não suporta declaração embutida no estilo json. Por exemplo, o seguinte não é compilado:

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

Eu queria uma solução que tivesse o seguinte:

  • Todos os objetos de dados são necessários e devem ser preenchidos pelo construtor.
  • Não há necessidade de fornecer padrões.
  • Pode usar funções dentro da classe.

Aqui está a maneira que eu faço:

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 as propriedades no construtor são obrigatórias e não podem ser omitidas sem um erro do compilador.

É dependente do OnlyDataque filtra getFullName()as propriedades necessárias e é definido da seguinte maneira:

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

Limitações atuais dessa maneira:

  • Requer TypeScript 2.8
  • Aulas com getters / setters
VitalyB
fonte
Essa resposta parece mais próxima do ideal para mim. Perguntando se podemos fazer melhor com o
texto
1
Até onde eu sei, esse é o melhor caminho a seguir até hoje. Obrigado.
florian norbert bepunkt
Há um problema com esta solução, @VitalyB: assim que os métodos têm parâmetros, isso é interrompido: Enquanto getFullName () {return "bar"} funciona, getFullName (str: string): string {return str} não funciona
florian norbert bepunkt
@floriannorbertbepunkt O que exatamente não está funcionando para você? Parece estar funcionando bem para mim ...
rfgamaral 05/03
3

Esta é outra solução:

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

Você pode ter uma classe com campos opcionais (marcados com?) E um construtor que recebe uma instância da mesma 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" }),
];

Nesse caso, você não poderá omitir os campos obrigatórios. Isso fornece controle refinado sobre a construção do objeto.

Você pode usar o construtor com o tipo Parcial, conforme observado em outras respostas:

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

O problema é que todos os campos se tornam opcionais e isso não é desejável na maioria dos casos.

Alex
fonte
1

A maneira mais fácil de fazer isso é com a conversão de tipo.

return <MyClass>{ Field1: "ASD", Field2: "QWE" };
Peter Pompeii
fonte
7
Infelizmente, (1) isso não é conversão de tipo, mas uma afirmação de tipo em tempo de compilação , (2) a pergunta feita "como iniciar uma nova classe " (ênfase minha), e essa abordagem falhará em fazer isso. Seria certamente bom se o TypeScript tivesse esse recurso, mas infelizmente não é esse o caso.
John Weisz
onde está a definição de MyClass?
Jeb50
0

Se você estiver usando uma versão antiga do texto datilografado <2.1, poderá usar semelhante ao seguinte, que é basicamente o lançamento de qualquer objeto digitado:

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

NOTA: O uso desse método é bom apenas para modelos de dados, pois remove todos os métodos do objeto. Basicamente, está lançando qualquer objeto para um objeto digitado

Mohsen Tabareh
fonte
-1

se você deseja criar uma nova instância sem definir o valor inicial quando a instância

1- você tem que usar a classe não interface

2- você precisa definir o valor inicial ao criar a classe

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


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