Inicjalizatory TypeScript i pola

252

Jak zainicjować nową klasę w TStaki sposób (przykład, C#aby pokazać, czego chcę):

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

[edytuj]
Kiedy pisałem to pytanie, byłem czystym programistą .NET bez dużej wiedzy o JS. Również TypeScript był czymś zupełnie nowym, ogłoszonym jako nowy nadzbiór JavaScript oparty na języku C #. Dziś widzę, jak głupie było to pytanie.

W każdym razie, jeśli ktoś nadal szuka odpowiedzi, zapoznaj się z możliwymi rozwiązaniami poniżej.

Pierwszą rzeczą do odnotowania jest to, że w TS nie powinniśmy tworzyć pustych klas dla modeli. Lepszym sposobem jest utworzenie interfejsu lub typu (w zależności od potrzeb). Dobry artykuł od Todda Motto: https://ultimatecourses.com/blog/classes-vs-interfaces-in-typescript

ROZWIĄZANIE 1:

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

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

ROZWIĄZANIE 2:

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

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

ROZWIĄZANIE 3 (kiedy naprawdę potrzebujesz zajęć):

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

lub

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

Oczywiście w obu przypadkach typy rzutowania mogą nie być potrzebne ręcznie, ponieważ zostaną one usunięte z typu zwracanego przez funkcję / metodę.

Nickon
źródło
9
„Rozwiązanie”, które właśnie dodałeś do pytania, jest nieprawidłowe TypeScript lub JavaScript. Warto jednak zauważyć, że jest to najbardziej intuicyjna metoda.
Jacob Foshee,
1
@JacobFoshee niepoprawny JavaScript? Zobacz moje Narzędzia Chrome Dev: i.imgur.com/vpathu6.png (ale wizualna Code Studio lub inny maszynopis lintowane nieuchronnie narzekać)
Mars Robertson
5
@MichalStefanow OP zmodyfikował pytanie po tym, jak opublikowałem ten komentarz. Miałreturn new MyClass { Field1: "ASD", Field2: "QWE" };
Jacob Foshee

Odpowiedzi:

90

Aktualizacja

Od czasu napisania tej odpowiedzi pojawiły się lepsze sposoby. Zobacz pozostałe odpowiedzi poniżej, które mają więcej głosów i lepszą odpowiedź. Nie mogę usunąć tej odpowiedzi, ponieważ jest ona oznaczona jako zaakceptowana.


Stara odpowiedź

W kodepleksie TypeScript występuje problem, który opisuje to: Obsługa inicjatorów obiektów .

Jak już wspomniano, można to zrobić już za pomocą interfejsów w TypeScript zamiast klas:

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
źródło
81
W twoim przykładzie bob nie jest instancją klasy Person. Nie rozumiem, jak to jest równoważne z przykładem C #.
Jack Wester
3
nazwy interfejsów lepiej zacząć od
dużej litery
16
1. Osoba nie jest klasą, a 2. @JackWester ma rację, bob nie jest instancją Osoby. Spróbuj alert (bob instanceof Person); W tym kodzie przykład Osoba jest tylko dla celów asercji typu.
Jacques
15
Zgadzam się z Jackiem i Jaques i uważam, że warto to powtórzyć. Twój bob jest tego typu Person , ale wcale nie jest to przypadek Person. Wyobraź sobie, że Persontak naprawdę byłaby to klasa ze złożonym konstruktorem i szeregiem metod, takie podejście upadłoby na pierwszy plan. To dobrze, że wiele osób uznało twoje podejście za przydatne, ale nie jest to rozwiązanie tego pytania, jak zostało powiedziane, i równie dobrze możesz użyć klasy Osoba w twoim przykładzie zamiast interfejsu, byłby to ten sam typ.
Alex
26
Dlaczego jest to akceptowana odpowiedź, jeśli jest wyraźnie błędna?
Hendrik Wiese
447

Zaktualizowano 07.12.2016: Typescript 2.1 wprowadza mapowane typy i zapewnia Partial<T>, co pozwala to zrobić ....

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

Oryginalna odpowiedź:

Moje podejście polega na zdefiniowaniu oddzielnej fieldszmiennej przekazywanej do konstruktora. Sztuką jest ponowne zdefiniowanie wszystkich pól klasy dla tego inicjalizatora jako opcjonalnych. Kiedy obiekt jest tworzony (z ustawieniami domyślnymi), po prostu przypisujesz obiekt inicjalizujący 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);
    }
}

lub zrób to ręcznie (nieco bezpieczniej):

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

stosowanie:

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

i wyjście konsoli:

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 }   

Daje to podstawową inicjalizację bezpieczeństwa i właściwości, ale wszystko jest opcjonalne i może być nieczynne. Zostaniesz pozostawiony domyślnym ustawieniom klasy, jeśli nie przejdziesz pola.

Możesz także mieszać go z wymaganymi parametrami konstruktora - trzymaj fieldssię końca.

Myślę, że mniej więcej w stylu C #, jak zamierzacie ( faktyczna składnia inicjalizacji pola została odrzucona ). Wolałbym właściwy inicjator pola, ale nie wygląda na to, żeby to się jeszcze wydarzyło.

Dla porównania, jeśli zastosujesz metodę rzutowania, twój obiekt inicjalizujący musi mieć WSZYSTKIE pola dla typu, na który rzutujesz, oraz nie może uzyskać żadnych funkcji specyficznych dla klasy (lub pochodnych) utworzonych przez samą klasę.

Meirion Hughes
źródło
1
+1. To faktycznie tworzy instancję klasy (czego większość z tych rozwiązań nie ma), zachowuje całą funkcjonalność wewnątrz konstruktora (bez Object.create lub innego cruft poza) i jawnie podaje nazwę właściwości zamiast polegać na porządkowaniu paramów ( moje osobiste preferencje tutaj). Traci jednak bezpieczeństwo typu / kompilacji między parametrami a właściwościami.
Fiddles,
1
@ user1817787 prawdopodobnie lepiej byłoby zdefiniować wszystko, co ma wartość domyślną jako opcjonalną w samej klasie, ale przypisać wartość domyślną. Więc nie używaj Partial<>, po prostu Person- to będzie wymagało przekazania obiektu, który ma wymagane pola. To powiedziawszy, zobacz tutaj pomysły (patrz Wybierz), które ograniczają mapowanie typów do niektórych pól.
Meirion Hughes
2
To naprawdę powinna być odpowiedź. To najlepsze rozwiązanie tego problemu.
Ascherer
9
to jest ZŁOTY fragment kodupublic constructor(init?:Partial<Person>) { Object.assign(this, init); }
kuncevic.dev
3
Jeśli Object.assign wyświetla błąd, który był dla mnie, proszę zobaczyć tę SO odpowiedź: stackoverflow.com/a/38860354/2621693
Jim Yarbro
27

Poniżej znajduje się rozwiązanie, które łączy krótszą aplikację w Object.assigncelu dokładniejszego modelowania oryginalnego C#wzoru.

Najpierw jednak przejrzyjmy oferowane dotychczas techniki, które obejmują:

  1. Skopiuj konstruktory, które akceptują obiekt i stosują go do Object.assign
  2. Sprytna Partial<T>sztuczka w konstruktorze kopiowania
  3. Zastosowanie „rzutowania” na POJO
  4. Wykorzystanie Object.createzamiastObject.assign

Oczywiście, każdy ma swoje zalety / wady. Modyfikacja klasy docelowej w celu utworzenia konstruktora kopii nie zawsze może być opcją. A „rzutowanie” traci wszelkie funkcje związane z typem celu. Object.createwydaje się mniej atrakcyjna, ponieważ wymaga dość szczegółowej mapy deskryptorów właściwości.

Najkrótsza, ogólna odpowiedź

Oto kolejne podejście, które jest nieco prostsze, zachowuje definicję typu i powiązane prototypy funkcji oraz dokładniej modeluje zamierzony C#wzorzec:

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

Otóż ​​to. Jedynym dodatkiem do C#wzorca są Object.assign2 nawiasy i przecinek. Sprawdź działający przykład poniżej, aby potwierdzić, że zachowuje prototypy funkcji typu. Nie wymaga konstruktorów ani sprytnych sztuczek.

Przykład roboczy

Ten przykład pokazuje, jak zainicjować obiekt za pomocą aproksymacji C#inicjalizatora pola:

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.
źródło
Podobnie jak ten, może utworzyć obiekt użytkownika z obiektu javascript
Dave Keane 30.04.19
1
6 i pół roku później zapomniałem o tym, ale nadal mi się podoba. Dzięki jeszcze raz.
PRMan
25

Możesz wpływać na anonimowy obiekt rzucony w typie swojej klasy. Bonus : W studiu wizualnym korzystasz w ten sposób z inteligencji :)

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

Edytować:

OSTRZEŻENIE Jeśli klasa ma metody, instancja klasy nie uzyska ich. Jeśli AClass ma konstruktor, nie zostanie on wykonany. Jeśli użyjesz instanceof AClass, otrzymasz fałsz.

Podsumowując, powinieneś używać interfejsu, a nie klasy . Najczęstszym zastosowaniem jest model domeny zadeklarowany jako Plain Old Objects. Rzeczywiście, w przypadku modelu domeny lepiej użyć interfejsu zamiast klasy. Interfejsy są używane w czasie kompilacji do sprawdzania typów i, w przeciwieństwie do klas, interfejsy są całkowicie usuwane podczas kompilacji.

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

var anObject: IModel = {
     Property1: "Value",
     Property2: "Value",
     PropertyBoolean: true,
     PropertyNumber: 1
 };
rdhainaut
źródło
20
Jeśli AClasszawarte metody, anInstancenie dostaną ich.
Monsignor
2
Również jeśli AClass ma konstruktor, nie zostanie on wykonany.
Michael Erickson
1
Również jeśli to zrobisz anInstance instanceof AClass, otrzymasz falsew czasie wykonywania.
Lucero
15

Sugeruję podejście, które nie wymaga 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();

Kluczowe punkty:

  • Maszynopis 2.1 nie jest wymagany, Partial<T> nie jest wymagany
  • Obsługuje funkcje (w porównaniu z prostym asercją typu, która nie obsługuje funkcji)
VeganHunter
źródło
1
Nie respektuje obowiązkowych pól: new Person(<Person>{});(ponieważ casting) i też jest jasne; używanie częściowe <T> obsługuje funkcje. Ostatecznie, jeśli masz wymagane pola (plus funkcje prototypowe), musisz zrobić: init: { name: string, address?: string, age: number }i upuścić rzutowanie.
Meirion Hughes
Również gdy otrzymamy mapowanie typu warunkowego , będziesz mógł zmapować tylko funkcje na częściowe i zachować właściwości bez zmian. :)
Meirion Hughes
Zamiast wyrafinowany classnastępnie var, jeśli to zrobię var dog: {name: string} = {name: 'will be assigned later'};, to kompiluje i działa. Jakikolwiek niedobór lub problem? Och, dogma bardzo mały zakres i jest specyficzny, co oznacza tylko jeden przypadek.
Jeb50
11

Byłbym bardziej skłonny zrobić to w ten sposób, używając (opcjonalnie) automatycznych właściwości i wartości domyślnych. Nie zasugerowałeś, że dwa pola są częścią struktury danych, dlatego właśnie wybrałem tę metodę.

Możesz mieć właściwości w klasie, a następnie przypisać je w zwykły sposób. I oczywiście mogą być lub nie być wymagane, więc to także coś innego. Po prostu jest to taki miły cukier składniowy.

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
źródło
6
Moje najgłębsze przeprosiny. Ale tak naprawdę nie „zapytałeś o inicjatory pola”, więc naturalne jest założenie, że możesz być zainteresowany alternatywnymi sposobami odnowienia klasy w TS. Możesz podać nieco więcej informacji w swoim pytaniu, jeśli jesteś tak gotowy do głosowania.
Ralph Lavelle
Konstruktor +1 to droga do celu, o ile to możliwe; ale w przypadkach, gdy masz wiele pól i chcesz zainicjować tylko niektóre z nich, myślę, że moja odpowiedź ułatwia.
Meirion Hughes,
1
Jeśli masz wiele pól, inicjowanie takiego obiektu byłoby dość niewygodne, ponieważ przechodziłbyś przez konstruktor ścianę o bezimiennych wartościach. Nie oznacza to, że ta metoda nie ma żadnej wartości; najlepiej byłoby zastosować go do prostych obiektów z kilkoma polami. Większość publikacji mówi, że około czterech lub pięciu parametrów w sygnaturze członka jest górną granicą. Zwracam na to uwagę, ponieważ znalazłem link do tego rozwiązania na czyimś blogu podczas wyszukiwania inicjatorów TS. Mam obiekty z ponad 20 polami, które należy zainicjować do testów jednostkowych.
Dustin Cleveland,
11

W niektórych sytuacjach użycie może być dopuszczalne Object.create. Odwołanie do Mozilli zawiera polifill, jeśli potrzebujesz kompatybilności wstecznej lub chcesz uruchomić własną funkcję inicjalizacyjną.

Dotyczy twojego przykładu:

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

Przydatne scenariusze

  • Testy jednostkowe
  • Deklaracja wbudowana

W moim przypadku uznałem to za przydatne w testach jednostkowych z dwóch powodów:

  1. Podczas testowania oczekiwań często chcę stworzyć cienki obiekt jako oczekiwanie
  2. Frameworki testów jednostkowych (takie jak Jasmine) mogą porównywać prototyp obiektu ( __proto__) i zawieść test. Na przykład:
var actual = new MyClass();
actual.field1 = "ASD";
expect({ field1: "ASD" }).toEqual(actual); // fails

Wynik niepowodzenia testu jednostkowego nie dostarczy pojęcia o tym, co jest niezgodne.

  1. W testach jednostkowych mogę selektywnie wybierać obsługiwane przeglądarki

Wreszcie rozwiązanie zaproponowane na stronie http://typescript.codeplex.com/workitem/334 nie obsługuje wbudowanej deklaracji typu json. Na przykład nie można skompilować:

var o = { 
  m: MyClass: { Field1:"ASD" }
};
Jacob Foshee
źródło
9

Chciałem rozwiązania, które miałoby następujące:

  • Wszystkie obiekty danych są wymagane i muszą zostać wypełnione przez konstruktora.
  • Nie trzeba podawać wartości domyślnych.
  • Może korzystać z funkcji wewnątrz klasy.

Oto jak to robię:

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

Wszystkie właściwości konstruktora są obowiązkowe i nie można ich pominąć bez błędu kompilatora.

To zależy od tego, OnlyDataktóry filtrujegetFullName() wymagane właściwości i jest zdefiniowane w następujący sposób:

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

Obecne ograniczenia tego sposobu:

  • Wymaga TypeScript 2.8
  • Zajęcia z pobierającymi / ustawiającymi
Witalij B.
źródło
Ta odpowiedź wydaje mi się najbliższa ideału. Zastanawiam się, czy da się lepiej z pismem maszynowym 3+
RyanM
1
O ile wiem, jest to najlepszy sposób na dzisiaj. Dziękuję Ci.
florian norbert bepunkt
Jest jeden problem z tym rozwiązaniem, @VitalyB: Gdy tylko metody mają parametry, problem się psuje: Podczas gdy getFullName () {return "bar"} działa, getFullName (str: string): string {return str} nie działa
florian norbert bepunkt
@floriannorbertbepunkt Co dokładnie nie działa dla Ciebie? Wydaje mi się, że działa dobrze ...
rfgamaral
3

To jest inne rozwiązanie:

return {
  Field1 : "ASD",
  Field2 : "QWE" 
} as myClass;
rostamiani
źródło
2

Możesz mieć klasę z opcjonalnymi polami (oznaczonymi?) I konstruktor, który odbiera instancję tej samej klasy.

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

W takim przypadku nie będzie można pominąć wymaganych pól. Zapewnia to precyzyjną kontrolę nad konstrukcją obiektu.

Możesz użyć konstruktora z typem Częściowym, jak zauważono w innych odpowiedziach:

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

Problem polega na tym, że wszystkie pola stają się opcjonalne i w większości przypadków nie jest pożądane.

Alex
źródło
1

Najłatwiej to zrobić za pomocą rzutowania typu.

return <MyClass>{ Field1: "ASD", Field2: "QWE" };
Piotr Pompeje
źródło
7
Niestety (1) nie jest to rzutowanie typu, ale stwierdzenie typu w czasie kompilacji , (2) pytanie brzmiało: „jak zainicjować nową klasę (moje wyróżnienie), a to podejście nie da tego osiągnąć. Z pewnością byłoby miło, gdyby TypeScript miał tę funkcję, ale niestety tak nie jest.
John Weisz
gdzie jest definicja MyClass?
Jeb50,
0

Jeśli używasz starej wersji maszynopisu <2.1, możesz użyć podobnej do poniższej, która polega na rzutowaniu dowolnego na wpisany obiekt:

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

UWAGA: Użycie tej metody jest dobre tylko w przypadku modeli danych, ponieważ spowoduje usunięcie wszystkich metod w obiekcie. Zasadniczo polega to na rzutowaniu dowolnego obiektu na wpisany obiekt

Mohsen Tabareh
źródło
-1

jeśli chcesz utworzyć nowe wystąpienie bez ustawiania wartości początkowej, gdy wystąpienie

1- musisz użyć klasy, a nie interfejsu

2- musisz ustawić wartość początkową podczas tworzenia klasy

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


student: IStudentDTO = new IStudentDTO();
hosam hemaily
źródło