TypeScript и инициализаторы полей


252

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

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

[править]
Когда я писал этот вопрос, я был чистым разработчиком .NET без особых знаний JS. Также TypeScript был чем-то совершенно новым, объявленным как новый расширенный набор JavaScript на основе 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. Но важно отметить, что это наиболее интуитивно понятная вещь.
Джейкоб Фоши

1
@JacobFoshee не действительный JavaScript? См. Мой Chrome Dev Tools: i.imgur.com/vpathu6.png (но код Visual Studio или любой другой напечатанный TypeScript неизбежно будут жаловаться)
Марс Робертсон,

5
@MichalStefanow ОП отредактировал вопрос после того, как я опубликовал этот комментарий. У него былreturn new MyClass { Field1: "ASD", Field2: "QWE" };
Джейкоб Фоши

Ответы:


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
Имена интерфейсов лучше начинать с заглавной буквы «I»
Kiarash

16
1. Person - это не класс, и 2. @JackWester прав, Боб не является представителем Person. Попробуйте оповещение (боб экземпляр Person); В этом примере кода Person существует только для целей утверждения типа.
Жак

15
Я согласен с Джеком и Жаком и думаю, что стоит повторить. Ваш боб относится к типу Person , но это совсем не случай Person. Представьте себе, что на Personсамом деле это был бы класс со сложным конструктором и набором методов, этот подход потерпел бы неудачу. Хорошо, что группа людей сочла ваш подход полезным, но это не решение вопроса, как он сформулирован, и вы также можете использовать класс Person в вашем примере вместо интерфейса, это будет тот же тип.
Алекс

26
Почему это принятый ответ, когда он явно неверен?
Хендрик Визе

448

Обновлено 12/12/2016: Typescript 2.1 представляет Mapped Types и предоставляет 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
Джим Ярбро

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.assignдве круглые скобки и запятая. Посмотрите рабочий пример ниже, чтобы убедиться, что он поддерживает прототипы функций типа. Никаких конструкторов и хитрых трюков.

Рабочий пример

В этом примере показано, как инициализировать объект с помощью аппроксимации 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
Через 6 с половиной лет я забыл это, но мне все еще нравится. Еще раз спасибо.
PRMan

25

Вы можете воздействовать на анонимный объект, приведенный к вашему типу класса. Бонус : В визуальной студии вы получаете пользу от intellisense таким образом :)

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

Редактировать:

ПРЕДУПРЕЖДЕНИЕ Если у класса есть методы, экземпляр вашего класса не получит их. Если у AClass есть конструктор, он не будет выполнен. Если вы используете instanceof AClass, вы получите false.

В заключение, вы должны использовать интерфейс, а не класс . Наиболее распространенное использование для модели предметной области, объявленной как Простые Старые Объекты. Действительно, для модели предметной области лучше использовать интерфейс, а не класс. Интерфейсы используются во время компиляции для проверки типов, и, в отличие от классов, интерфейсы полностью удаляются во время компиляции.

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во время выполнения.
Лусеро

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

ключевые моменты:

  • Машинопись 2.1 не требуется, Partial<T>не требуется
  • Он поддерживает функции (по сравнению с утверждением простого типа, которое не поддерживает функции)

1
Это не относится к обязательным полям: new Person(<Person>{});(потому что приведение) и должно быть ясно; использование Partial <T> поддерживает функции. В конечном счете, если у вас есть обязательные поля (плюс функции прототипа), вам нужно будет сделать: init: { name: string, address?: string, age: number }и отбросить приведение.
Мейрион Хьюз

Также, когда мы получим отображение условного типа, вы сможете отобразить только функции на частичные и сохранить свойства как есть. :)
Мейрион Хьюз

Вместо сложного 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. Фреймворки модульных тестов (например, Jasmine) могут сравнивать прототип объекта ( __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
Насколько я могу судить, это лучший путь на сегодняшний день. Спасибо.
Флориан Норберт Бепункт

Есть одна проблема с этим решением, @VitalyB: как только методы имеют параметры, они ломаются: пока getFullName () {return "bar"} работает, getFullName (str: string): string {return str} не работает
Флориан Норберт Бепункт

@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

Если вы используете старую версию машинописного текста <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.