Ініціалізатори TypeScript і поля


252

Як запросити новий клас TSтаким чином (наприклад, C#щоб показати, що я хочу):

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

[редагувати]
Коли я писав це питання, я був чистим розробником .NET без особливих знань JS. Також TypeScript був чимось абсолютно новим, оголошеним як новий набір C # на базі C #. Сьогодні я бачу, наскільки дурним було це питання.

У будь-якому випадку, якщо хтось ще шукає відповідь, будь ласка, подивіться на можливі рішення нижче.

Перше, що слід зазначити, в TS не слід створювати порожні класи для моделей. Кращим способом є створення інтерфейсу або типу (залежно від потреб). Хороша стаття від Тодда Мотто: https://ultimatecourses.com/blog/classes-vs-interfaces-in-typescript

РІШЕННЯ 1:

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

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

РІШЕННЯ 2:

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

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

РІШЕННЯ 3 (коли вам справді потрібен клас):

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

або

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

Звичайно, в обох випадках вам можуть не знадобитися типи кастингу вручну, оскільки вони будуть вирішені з типу повернення функції / методу.


9
Щойно додане вами питання "рішення" недійсне TypeScript або JavaScript. Але цінно зазначити, що спробувати це найінтуїтивніше.
— Jacob Foshee

1
@JacobFoshee недійсний JavaScript? Перегляньте мої інструменти для розробників Chrome: i.imgur.com/vpathu6.png (але Visual Studio Studio або будь-який інший вкладений TypeScript неминуче поскаржиться)
— Mars Robertson

5
@MichalStefanow ОП відредагував це питання після того, як я опублікував цей коментар. Він єreturn new MyClass { Field1: "ASD", Field2: "QWE" };
— Jacob Foshee

Відповіді:


90

Оновлення

З моменту написання цієї відповіді з'явилися кращі способи. Будь ласка, дивіться інші відповіді нижче, у яких більше голосів і краща відповідь. Я не можу видалити цю відповідь, оскільки вона позначена як прийнята.


Стара відповідь

У кодовому пакеті TypeScript є проблема, яка описує це: Підтримка ініціалізаторів об'єктів .

Як зазначено, ви вже можете це зробити, використовуючи інтерфейси в TypeScript замість класів:

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

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

81
У вашому прикладі bob - це не екземпляр класу Person. Я не бачу, як це еквівалентно прикладу C #.
— Джек Вестер

3
Імена інтерфейсу краще починати з великої літери "Я"
— Kiarash

16
1. Людина не клас, а 2. @JackWester правий, bob не є екземпляром Person. Спробуйте сповістити (bob instanceof Person); У цьому прикладі коду Person присутній лише для цілей затвердження типу.
— Жак

15
Я погоджуюся з Джеком і Жаком, і я вважаю, що варто повторити. Ваш боб типу Person , але це зовсім не примірник Person. Уявіть, що Personнасправді це був би клас зі складним конструктором та безліччю методів, такий підхід впав би на обличчя. Добре, що група людей вважає ваш підхід корисним, але це не є вирішенням питання, як це зазначено, і ви могли б так само добре використовувати класу Person у своєму прикладі замість інтерфейсу, це був би той самий тип.
— Олексій

26
Чому це прийнята відповідь, коли явно неправильно?
— Хендрік Візе

447

Оновлено 07.12.2016: Typescript 2.1 представляє типи Mapped та надає Partial<T>, що дозволяє вам це зробити ....

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

Оригінальний відповідь:

Мій підхід полягає у визначенні окремої fieldsзмінної, яку ви передаєте конструктору. Трюк полягає в тому, щоб переосмислити всі поля класу для цього ініціалізатора як необов’язкового. Коли об’єкт створений (з його за замовчуванням), ви просто призначите об'єкт ініціалізатора 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);
    }
}

або зробити це вручну (трохи безпечніше):

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

використання:

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

і консольний вихід:

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 }   

Це дає вам основну безпеку та ініціалізацію властивостей, але все це необов'язково і може бути поза встановленням. Якщо ти не передаєш поле, типові параметри класу залишаються в спокої.

Ви також можете змішати його з необхідними параметрами конструктора - приклейте fieldsна кінець.

Приблизно до стилю C #, про який ви збираєтеся зрозуміти ( фактичний синтаксис field-init був відхилений ). Я б більше віддав перевагу належному ініціалізатору поля, але, схоже, це ще не відбудеться.

Для порівняння: Якщо ви використовуєте підхід кастингу, ваш об'єкт-ініціалізатор повинен мати ВСІ поля для типу, який ви передаваєте, плюс не отримувати жодних специфічних для класу функцій (або виводів), створених самим класом.


1
+1. Це фактично створює екземпляр класу (якого немає в більшості цих рішень), зберігає всю функціональність всередині конструктора (немає Object.create або іншої суворої зовнішності) і явно заявляє ім'я властивості замість того, щоб покладатися на впорядкування парам ( мої особисті переваги тут). Однак він втрачає тип / компілює безпеку між парами та властивостями.
— Fiddles

1
@ user1817787 вам, мабуть, буде краще визначити все, що має за замовчуванням як необов’язковий у самому класі, але призначити за замовчуванням. Тоді не використовуйте Partial<>, просто Person- це вимагатиме від вас передачі об'єкта, який має обов'язкові поля. тут сказано, дивіться тут ідеї (Див. Вибір), які обмежують Тип картографування певними полями.
— Меіріон Х'юз

2
Це справді має бути відповіддю. Це найкраще рішення цього питання.
— Ascherer

9
це ЗОЛОТИЙ фрагмент кодуpublic constructor(init?:Partial<Person>) { Object.assign(this, init); }
— kuncevic.dev

3
Якщо Object.assign показує помилку , як це було для мене, будь ласка , см цей SO відповідь: stackoverflow.com/a/38860354/2621693
— Джим Yarbro

27

Нижче наведено рішення, яке поєднує коротше застосування Object.assignдля більш детального моделювання оригінального C#малюнка.

Але спочатку давайте розглянемо запропоновані методики, які включають:

  1. Скопіюйте конструктори, які приймають об'єкт і застосовують його до Object.assign
  2. Розумний Partial<T>трюк в конструкторі копій
  3. Використання "кастингу" проти POJO
  4. Використання Object.createзамість цьогоObject.assign

Звичайно, у кожного є свої плюси / мінуси. Змінення цільового класу для створення конструктора копій не завжди може бути варіантом. І "лиття" втрачає будь-які функції, пов'язані з цільовим типом. Object.createвидається менш привабливим, оскільки для цього потрібна досить детальна карта дескриптора властивостей.

Найкоротший, загальноприйнятий відповідь

Отже, ось ще один підхід, який є дещо простішим, підтримує визначення типу та пов'язані з ним прототипи функцій та більш детально моделює намічений C#шаблон:

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

Це воно. Єдине доповнення над C#візерунком - це Object.assign2 дужки та кома. Перегляньте наведений нижче робочий приклад, щоб переконатися, що він підтримує прототипи функцій типу. Ніяких конструкторів не потрібно, і розумних хитрощів.

Робочий приклад

Цей приклад показує, як ініціалізувати об’єкт за допомогою наближення C#ініціалізатора поля:

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


Як і цей, можна створити об’єкт користувача з об’єкта javascript
— Дейв Кін

1
Через півтора року я забув це, але мені все одно подобається. Знову дякую.
— PRMan

25

Ви можете вплинути на анонімний об’єкт, який передається у тип вашого класу. Бонус : У візуальній студії ви користуєтеся інтелігенцією таким чином :)

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

Редагувати:

ПОПЕРЕДЖЕННЯ Якщо у класі є методи, екземпляр вашого класу їх не отримає. Якщо AClass має конструктор, він не буде виконуватися. Якщо ви використовуєте instanceof AClass, ви отримаєте помилку.

На закінчення ви повинні використовувати інтерфейс, а не клас . Найбільш поширене використання для доменної моделі, оголошеної як звичайні старі об’єкти. Дійсно, для доменної моделі вам краще використовувати інтерфейс, а не клас. Інтерфейси використовуються під час компіляції для перевірки типів і на відміну від класів, інтерфейси повністю видаляються під час компіляції.

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

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

20
Якщо AClassмістяться методи, їх anInstanceби не отримати.
— Монсеньйор

2
Також якщо AClass має конструктор, він не буде виконуватися.
— Майкл Еріксон,

1
Крім того, якщо ви це зробите, anInstance instanceof AClassви отримаєте falseпід час виконання.
— Lucero

15

Я пропоную підхід, який не вимагає 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();

Ключові моменти:

  • Typescript 2.1 не потрібно, Partial<T>не потрібно
  • Він підтримує функції (порівняно з твердженням простого типу, яке не підтримує функції)

1
Він не дотримується обов'язкових полів: new Person(<Person>{});(бо кастинг) і також бути зрозумілим; за допомогою часткової <T> підтримує функції. Зрештою, якщо у вас є необхідні поля (плюс прототипні функції), вам потрібно буде виконати: init: { name: string, address?: string, age: number }і скинути склад.
— Meirion Hughes

Крім того, коли ми отримаємо відображення умовного типу, ви зможете відобразити лише функції на частки та зберегти властивості такими, які є. :)
— Меїріон Х'юз

Замість витонченого classтоді var, якщо я це роблю var dog: {name: string} = {name: 'will be assigned later'};, він компілює і працює. Якийсь недолік чи проблема? О, dogмає дуже малий обсяг і специфічний, що означає лише один екземпляр.
— Jeb50

11

Я хотів би зробити це таким чином, використовуючи (необов'язково) автоматичні властивості та параметри за замовчуванням. Ви не припускали, що ці два поля є частиною структури даних, тому я вибрав саме цей шлях.

Ви можете мати властивості класу, а потім призначити їм звичайний спосіб. І очевидно, вони можуть бути, а може і не вимагати, тож це теж щось інше. Просто це такий приємний синтаксичний цукор.

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
Мої найглибші вибачення. Але ви насправді не "запитували про ініціалізатори поля", тому природно припустити, що вас можуть зацікавити альтернативні способи нововведення класу в TS. Ви можете дати трохи більше інформації у своєму запитанні, якщо ви так готові подати заявку.
— Ральф Лавель

+1 конструктор - це шлях, куди це можливо; але у випадках, коли у вас багато полів і хочете лише ініціалізувати деякі з них, я думаю, моя відповідь полегшує справи.
— Меїріон Х'юз

1
Якщо у вас багато полів, ініціалізація такого об’єкта була б досить непростою, оскільки ви передавали б конструктору стіну безіменних значень. Це не означає, що цей метод не має жодних заслуг; найкраще використовувати для простих об’єктів з кількома полями. Більшість публікацій кажуть, що біля чотирьох або п’яти параметрів у підписі члена є верхньою межею. Тільки вказуючи на це, тому що я знайшов посилання на це рішення в чиємусь блозі під час пошуку TS ініціалізаторів. У мене є об’єкти з 20+ полями, які потрібно ініціалізувати для одиничних тестів.
— Дастін Клівленд

11

У деяких сценаріях це може бути прийнятним Object.create. Посилання на Mozilla включає полізаповнення, якщо вам потрібна зворотна сумісність або ви хочете виконувати власну функцію ініціалізатора.

Застосовано до вашого прикладу:

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

Корисні сценарії

  • Тестові одиниці
  • Вбудована декларація

У моєму випадку я вважав це корисним в одиничних тестах з двох причин:

  1. Під час тестування очікувань я часто хочу створити тонкий предмет як очікування
  2. Тестові рамки блоку (наприклад, Жасмін) можуть порівнювати прототип об'єкта ( __proto__) і провалити тест. Наприклад:
var actual = new MyClass();
actual.field1 = "ASD";
expect({ field1: "ASD" }).toEqual(actual); // fails

Вихід з ладу тестового блоку не дасть поняття про те, що не відповідає.

  1. В одиничних тестах я можу бути вибірковим щодо того, які браузери я підтримую

Нарешті, рішення, запропоноване на веб-сайті http://typescript.codeplex.com/workitem/334 , не підтримує вбудоване оголошення в стилі json. Наприклад, наступне не компілюється:

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

9

Я хотів вирішити таке рішення:

  • Усі об'єкти даних необхідні і повинні заповнюватися конструктором.
  • Не потрібно надавати за замовчуванням.
  • Можна використовувати функції всередині класу.

Ось як я це роблю:

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

Усі властивості в конструкторі є обов'язковими і не можуть бути опущені без помилки компілятора.

Це залежить від того, OnlyDataщо фільтрує з getFullName()необхідних властивостей і визначається так:

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

Поточні обмеження цього способу:

  • Потрібен TypeScript 2.8
  • Заняття з геттерами / сеттерами

Ця відповідь мені здається найбільш близькою до ідеальної. Цікаво, чи зможемо ми краще зробити з машинописом 3+
— RyanM

1
Наскільки я можу сказати, це найкращий шлях вперед на сьогодні. Дякую.
— florian norbert bepunkt

Існує одна проблема з цим рішенням, @VitalyB: Як тільки у методів є параметри, це порушується: Хоча getFullName () {return "bar"} працює, getFullName (str: string): string {return str} не працює
— florian norbert bepunkt

@floriannorbertbepunkt Що саме для вас не працює? Здається, для мене це добре працює ...
— rfgamaral


2

У вас може бути клас із необов’язковими полями (позначений знаком?) Та конструктор, який отримує екземпляр того ж класу.

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

У цьому випадку ви не зможете опустити необхідні поля. Це дає точний дрібний контроль над будівництвом об'єкта.

Ви можете використовувати конструктор типу Partial, як зазначено в інших відповідях:

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

Проблема полягає в тому, що всі поля стають необов’язковими, і це не бажано в більшості випадків.


1

Найпростіший спосіб зробити це за допомогою кастингу типів.

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

7
На жаль, (1) це не типовий кастинг, а затвердження типу компіляції в часі ; (2) питання задало питання "як запустити новий клас " (моє наголос), і такий підхід цього не зможе досягти. Було б, звичайно, добре, якби TypeScript мав цю функцію, але, на жаль, це не так.
— Джон Вайс

де визначення MyClass?
— Jeb50

0

Якщо ви використовуєте стару версію typecript <2.1, ви можете використовувати аналогічну наступній, яка в основному є кастингом будь-якого введеного об'єкта:

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

ПРИМІТКА. Використання цього методу корисне лише для моделей даних, оскільки він видалить усі методи в об'єкті. Це в основному закидання будь-якого об'єкта на введений об'єкт


-1

якщо ви хочете створити новий екземпляр без встановленого початкового значення при екземплярі

1- ви повинні використовувати клас не інтерфейс

2- Ви повинні встановити початкове значення при створенні класу

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


student: IStudentDTO = new IStudentDTO();
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.