Typescript: Anfrage: Klassendekorateur-Mutation

Erstellt am 20. Sept. 2015  ·  231Kommentare  ·  Quelle: microsoft/TypeScript

Wenn wir dies zum richtigen Typcheck bringen können, hätten wir eine perfekte Unterstützung für Boilerplate-freie Mixins:

declare function Blah<T>(target: T): T & {foo: number}

<strong i="6">@Blah</strong>
class Foo {
    bar() {
        return this.foo; // Property 'foo' does not exist on type 'Foo'
    }
}

new Foo().foo; // Property 'foo' does not exist on type 'Foo'
Needs Proposal Suggestion

Hilfreichster Kommentar

Dasselbe wäre nützlich für Methoden:

class Foo {
  <strong i="6">@async</strong>
  bar(x: number) {
    return x || Promise.resolve(...);
  }
}

Der asynchrone Decorator soll den Rückgabetyp in Promise<any> ändern.

Alle 231 Kommentare

Dasselbe wäre nützlich für Methoden:

class Foo {
  <strong i="6">@async</strong>
  bar(x: number) {
    return x || Promise.resolve(...);
  }
}

Der asynchrone Decorator soll den Rückgabetyp in Promise<any> ändern.

@Gaelan , das ist genau das, was wir hier brauchen! Es würde Mixins zu einer natürlichen Arbeit machen.

class asPersistent {
  id: number;
  version: number;
  sync(): Promise<DriverResponse> { ... }
  ...
}

function PersistThrough<T>(driver: { new(): Driver }): (t: T) => T & asPersistent {
  return (target: T): T & asPersistent {
    Persistent.call(target.prototype, driver);
    return target;
  }
}

@PersistThrough(MyDBDriver)
Article extends TextNode {
  title: string;
}

var article = new Article();
article.title = 'blah';

article.sync() // Property 'sync' does not exist on type 'Article'

+1 dafür. Obwohl ich weiß, dass dies schwer zu implementieren ist und wahrscheinlich schwieriger, eine Einigung über die Dekorateur-Mutationssemantik zu erzielen.

+1

Wenn der Hauptvorteil davon darin besteht, zusätzliche Member in die Typsignatur einzuführen, können Sie dies bereits mit der Schnittstellenzusammenführung tun:

interface Foo { foo(): number }
class Foo {
    bar() {
        return this.foo();
    }
}

Foo.prototype.foo = function() { return 10; }

new Foo().foo();

Wenn der Decorator eine tatsächliche Funktion ist, die der Compiler aufrufen muss, um die Klasse zwingend zu mutieren, scheint dies meiner Meinung nach keine idiomatische Sache in einer typsicheren Sprache zu sein.

@masaeedu Kennen Sie eine Problemumgehung, um der dekorierten Klasse ein statisches Mitglied hinzuzufügen?

@davojan Sicher. Bitte schön:

class A { }
namespace A {
    export let foo = function() { console.log("foo"); }
}
A.foo();

Es wäre auch nützlich, _mehrere_ Eigenschaften in eine Klasse einführen zu können, wenn eine Methode dekoriert wird (z. B. ein Helfer, der einen zugehörigen Setter für einen Getter generiert, oder etwas in dieser Richtung).

Die React-Redux-Typisierungen für connect nehmen eine Komponente und geben eine modifizierte Komponente zurück, deren Requisiten die durch Redux empfangenen verbundenen Requisiten nicht enthalten, aber es scheint, dass TS ihre connect -Definition nicht als erkennt ein Dekorateur aufgrund dieses Problems. Hat jemand eine Problemumgehung?

Ich denke, die Typdefinition ClassDecorator muss geändert werden.

Derzeit sind es declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void; . Vielleicht könnte man es ändern

declare type MutatingClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
declare type WrappingClassDecorator = <TFunction extends Function, TDecoratorFunction extends Function>(target: TFunction) => TDecoratorFunction;
declare type ClassDecorator = MutatingClassDecorator | WrappingClassDecorator;

Offensichtlich ist die Benennung scheiße und ich habe keine Ahnung, ob so etwas funktionieren wird (ich versuche gerade, eine Babel-App in Typoskript umzuwandeln und treffe darauf).

@joyt Könnten Sie eine Spielplatzrekonstruktion des Problems bereitstellen? Ich verwende React-Redux nicht, aber wie ich bereits erwähnt habe, denke ich, dass alle Erweiterungen, die Sie für die Form eines Typs wünschen, mithilfe der Schnittstellenzusammenführung deklariert werden können.

@masaeedu hier ist eine grundlegende Aufschlüsselung der beweglichen Teile.

Grundsätzlich stellt der Decorator der React-Komponente eine Reihe von Requisiten zur Verfügung, sodass der generische Typ des Decorators eine Teilmenge der dekorierten Komponente ist, keine Obermenge.

Ich bin mir nicht sicher, ob dies hilfreich ist, habe aber versucht, ein nicht lauffähiges Beispiel zusammenzustellen, um Ihnen die im Spiel befindlichen Typen zu zeigen.

// React types
class Component<TProps> {
    props: TProps
}
class ComponentClass<TProps> {
}
interface ComponentDecorator<TOriginalProps, TOwnProps> {
(component: ComponentClass<TOriginalProps>): ComponentClass<TOwnProps>;
}

// Redux types
interface MapStateToProps<TStateProps, TOwnProps> {
    (state: any, ownProps?: TOwnProps): TStateProps;
}

// Fake react create class
function createClass(component: any, props: any): any {
}

// Connect wraps the decorated component, providing a bunch of the properies
// So we want to return a ComponentDecorator which exposes LESS than
// the original component
function connect<TStateProps, TOwnProps>(
    mapStateToProps: MapStateToProps<TStateProps, TOwnProps>
): ComponentDecorator<TStateProps, TOwnProps> {
    return (ComponentClass) => {
        let mappedState = mapStateToProps({
            bar: 'bar value'
        })
        class Wrapped {
            render() {
                return createClass(ComponentClass, mappedState)
            }
        }

        return Wrapped
    }
}


// App Types
interface AllProps {
    foo: string
    bar: string
}

interface OwnProps {
    bar: string
}

// This does not work...
// @connect<AllProps, OwnProps>(state => state.foo)
// export default class MyComponent extends Component<AllProps> {
// }

// This does
class MyComponent extends Component<AllProps> {
}
export default connect<AllProps, OwnProps>(state => state.foo)(MyComponent)
//The type exported should be ComponentClass<OwnProps>,
// currently the decorator means we have to export ComponentClass<AllProps>

Wenn Sie ein voll funktionsfähiges Beispiel wünschen, schlage ich vor, https://github.com/jaysoo/todomvc-redux-react-typescript oder ein anderes Beispiel-react/redux/typescript-Projekt herunterzuladen.

Gemäß https://github.com/wycats/javascript-decorators#class -declaration ist mein Verständnis, dass das vorgeschlagene declare type WrappingClassDecorator = <TFunction extends Function, TDecoratorFunction extends Function>(target: TFunction) => TDecoratorFunction; ungültig ist.

Die Spezifikation sagt:

@F("color")
<strong i="6">@G</strong>
class Foo {
}

heißt übersetzt:

var Foo = (function () {
  class Foo {
  }

  Foo = F("color")(Foo = G(Foo) || Foo) || Foo;
  return Foo;
})();

Also wenn ich es richtig verstehe, sollte folgendes gelten:

declare function F<T>(target: T): void;

<strong i="13">@F</strong>
class Foo {}

let a: Foo = new Foo(); // valid
class X {}
declare function F<T>(target: T): X;

<strong i="16">@F</strong>
class Foo {}

let a: X = new Foo(); // valid
let b: Foo = new Foo(); // INVALID
declare function F<T>(target: T): void;
declare function G<T>(target: T): void;

<strong i="19">@F</strong>
<strong i="20">@G</strong>
class Foo {}

let a: Foo = new Foo(); // valid
class X {}
declare function F<T>(target: T): void;
declare function G<T>(target: T): X;

<strong i="23">@F</strong>
<strong i="24">@G</strong>
class Foo {}

<strong i="25">@G</strong>
class Bar {}

<strong i="26">@F</strong>
class Baz {}

let a: Foo = new Foo(); // valid
let b: X = new Foo(); // INVALID
let c: X = new Bar(); // valid
let d: Bar = new Bar(); // INVALID
let e: Baz = new Baz(); // valid
class X {}
declare function F<T>(target: T): X;
declare function G<T>(target: T): void;

<strong i="29">@F</strong>
<strong i="30">@G</strong>
class Foo {}

<strong i="31">@G</strong>
class Bar {}

<strong i="32">@F</strong>
class Baz {}

let a: X = new Foo(); // valid
let b: Bar = new Bar(); // valid
let c: X = new Baz(); // valid
let d: Baz = new Baz(); // INVALID

@blai

Für dein Beispiel:

class X {}
declare function F<T>(target: T): X;

<strong i="9">@F</strong>
class Foo {}

let a: X = new Foo(); // valid
let b: Foo = new Foo(); // INVALID

Ich nehme an, Sie meinen, dass F eine Klasse zurückgibt, die X entspricht (und keine Instanz von X ist)? Z.B:

declare function F<T>(target: T): typeof X;

Für diesen Fall sollten die Behauptungen lauten:

let a: X = new Foo(); // valid
let b: Foo = new Foo(); // valid

Das Foo , das sich im Geltungsbereich dieser let -Anweisungen befindet, wurde vom Dekorateur mutiert. Das ursprüngliche Foo ist nicht mehr erreichbar. Es ist effektiv äquivalent zu:

let Foo = F(class Foo {});

@nevir Ja , du hast recht. Danke für die Klarstellung.

Nebenbei bemerkt scheint es relativ einfach zu sein, das Kontrollkästchen zum Ungültigmachen mutierter Klassentypen zu deaktivieren:

diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts
index 06591a7..2320aff 100644
--- a/src/compiler/checker.ts
+++ b/src/compiler/checker.ts
@@ -11584,10 +11584,6 @@ namespace ts {
           */
         function getDiagnosticHeadMessageForDecoratorResolution(node: Decorator) {
             switch (node.parent.kind) {
-                case SyntaxKind.ClassDeclaration:
-                case SyntaxKind.ClassExpression:
-                    return Diagnostics.Unable_to_resolve_signature_of_class_decorator_when_called_as_an_expression;
-
                 case SyntaxKind.Parameter:
                     return Diagnostics.Unable_to_resolve_signature_of_parameter_decorator_when_called_as_an_expression;
         }

         /** Check a decorator */
        function checkDecorator(node: Decorator): void {
             const signature = getResolvedSignature(node);
             const returnType = getReturnTypeOfSignature(signature);
             if (returnType.flags & TypeFlags.Any) {
@@ -14295,9 +14291,7 @@ namespace ts {
             let errorInfo: DiagnosticMessageChain;
             switch (node.parent.kind) {
                 case SyntaxKind.ClassDeclaration:
-                    const classSymbol = getSymbolOfNode(node.parent);
-                    const classConstructorType = getTypeOfSymbol(classSymbol);
-                    expectedReturnType = getUnionType([classConstructorType, voidType]);
+                    expectedReturnType = returnType;
                     break;

                 case SyntaxKind.Parameter:
         }

Aber ich bin nicht sachkundig genug, um den Compiler dazu zu bringen, die richtigen Typdefinitionen der mutierten Klasse auszugeben. Ich habe folgenden Test:

tests/cases/conformance/decorators/class/decoratorOnClass10.ts

// <strong i="10">@target</strong>:es5
// <strong i="11">@experimentaldecorators</strong>: true
class X {}
class Y {}

declare function dec1<T>(target: T): T | typeof X;
declare function dec2<T>(target: T): typeof Y;

<strong i="12">@dec1</strong>
<strong i="13">@dec2</strong>
export default class C {
}

var c1: X | Y = new C();
var c2: X = new C();
var c3: Y = new C();

Es generiert tests/baselines/local/decoratorOnClass10.types

=== tests/cases/conformance/decorators/class/decoratorOnClass10.ts ===
class X {}
>X : X

class Y {}
>Y : Y

declare function dec1<T>(target: T): T | typeof X;
>dec1 : <T>(target: T) => T | typeof X
>T : T
>target : T
>T : T
>T : T
>X : typeof X

declare function dec2<T>(target: T): typeof Y;
>dec2 : <T>(target: T) => typeof Y
>T : T
>target : T
>T : T
>Y : typeof Y

<strong i="17">@dec1</strong>
>dec1 : <T>(target: T) => T | typeof X

<strong i="18">@dec2</strong>
>dec2 : <T>(target: T) => typeof Y

export default class C {
>C : C
}

var c1: X | Y = new C();
>c1 : X | Y
>X : X
>Y : Y
>new C() : C
>C : typeof C

var c2: X = new C();
>c2 : X
>X : X
>new C() : C
>C : typeof C

var c3: Y = new C();
>c3 : Y
>Y : Y
>new C() : C
>C : typeof C

ich habe erwartet
>C: typeof C wird zu >C: typeof X | typeof Y

Für diejenigen, die an „react-redux“ connect als Fallstudie für dieses Feature interessiert sind, habe ich https://github.com/DefinitelyTyped/DefinitelyTyped/issues/9951 eingereicht, um das Problem an einem Ort zu verfolgen.

Ich habe alle Kommentare zu diesem Thema gelesen und bin auf die Idee gekommen, dass die Signatur des Dekorateurs nicht wirklich zeigt, was sie mit der umschlossenen Klasse tun kann.

Betrachten Sie dieses:

function decorator(target) {
    target.prototype.someNewMethod = function() { ... };
    return new Wrapper(target);
}

Es sollte so eingegeben werden:
declare function decorator<T>(target: T): Wrapper<T>;

Aber diese Signatur sagt uns nicht, dass der Dekorateur dem Prototyp des Ziels neue Dinge hinzugefügt hat.

Auf der anderen Seite sagt uns dieser hier nicht, dass der Dekorateur tatsächlich eine Verpackung zurückgegeben hat:
declare function decorator<T>(target: T): T & { someMethod: () => void };

Gibt es Neuigkeiten zu diesem Thema? Dies wäre super mächtig für die Metaprogrammierung!

Wie wäre es mit einem einfacheren Ansatz für dieses Problem? Für eine dekorierte Klasse binden wir den Klassennamen als syntaktischen Zucker an den Rückgabewert des Dekorators.

declare function Blah<T>(target: T): T & {foo: number}

<strong i="6">@Blah</strong>
class Foo {
    bar() {
        return this.foo; // Property 'foo' does not exist on type 'Foo'
    }
}

// is desugared to
const Foo = Blah(class Foo {
  // this.foo is not available here
})

new Foo.foo // foo is available here.

Hinsichtlich der Implementierung wird dies ein synthetisches Symbol für dekorierte Klassen einführen. Und der ursprüngliche Klassenname ist nur an den Bereich des Klassenkörpers gebunden.

@HerringtonDarkholme Ich denke, das wäre ein nett pragmatischer Ansatz, der den größten Teil der gewünschten Ausdruckskraft bieten würde. Großartige Idee!

Das will ich unbedingt einmal sehen

Ich schreibe oft eine Klasse für Angular 2 oder für Aurelia, die so aussieht:

import {Http} from 'aurelia-fetch-client';
import {User} from 'models';

// accesses backend routes for 'api/user'
<strong i="9">@autoinject</strong> export default class UserService {
  constructor(readonly http : Http) { }

  readonly resourceUrl = 'api/users';

  async get(id: number) {
    const response = await this.http.fetch(this.resourceUrl);
    const user = await response.json() as User;
    return user;
  }

  async post(id: number, model: { [K in keyof User]?: User[K] }) {
    const response = await this.http.post(`${this.resourceUrl}/`${id}`, model);
    return await response.json();
  }
}

Was ich schreiben möchte, ist so etwas wie
decorators/api-client.ts

import {Http} from 'aurelia-fetch-client';

export type Target = { name; new (...args): { http: Http }};

export default function apiClient<T extends { id: string }>(resourceUrl: string) {
  return (target: Target)  => {
    type AugmentedTarget = Target & { get(id: number): Promise<T>, post(id, model: Partial<T>) };
    const t = target as AugmentedTarget;
    t.prototype.get = async function (id: number) {
      const response = await this.http.fetch(resourceUrl);
      return await response.json() as T;
    }
  }
}

und dann könnte ich es gerne allgemein anwenden

import {Http} from 'aurelia-fetch-client';
import apiClient from ./decorators/api-client
import {User} from 'models';

@apiClient<User>('api/users') export default class UserService {
  constructor(readonly http : Http) { }
}

ohne Verlust der Typsicherheit. Dies wäre ein Segen für das Schreiben von sauberem, ausdrucksstarkem Code.

Wiederbelebung dieses Themas.

Jetzt, da #13743 draußen ist und Mixin-Unterstützung in der Sprache ist, ist dies eine super nützliche Funktion.

@HerringtonDarkholme ist für diesen Fall jedoch weniger geeignet, da der Rückgabetyp des Dekorateurs deklariert werden muss, gehen einige dynamische Funktionen verloren ...

@ahejlsberg , @mhegazy Glaubst du, das ist machbar?

Ich habe ein anderes Nutzungsszenario, von dem ich nicht sicher bin, ob es von dieser Konversation noch abgedeckt wird, aber wahrscheinlich unter denselben Oberbegriff fällt.

Ich möchte einen Methoden-Decorator implementieren, der den Typ der Methode vollständig ändert (nicht den Rückgabetyp oder die Parameter, sondern die gesamte Funktion). z.B

type AsyncTask<Method extends Function> = {
    isRunning(): boolean;
} & Method;

// Decorator definition...
function asyncTask(target, methodName, descriptor) {
    ...
}

class Order {
    <strong i="7">@asyncTask</strong>
    async save(): Promise<void> {
        // Performs an async task and returns a promise
        ...
    }
}

const order = new Order();

order.save();
order.save.isRunning(); // Returns true

In JavaScript durchaus möglich, das ist offensichtlich nicht das Problem, aber in TypeScript brauche ich den asyncTask Decorator, um den Typ der dekorierten Methode von () => Promise<void> in AsyncTask<() => Promise<void>> zu ändern.

Ziemlich sicher, dass dies jetzt nicht möglich ist und wahrscheinlich unter das Dach dieses Problems fällt?

@codeandcats Ihr Beispiel ist genau der gleiche Anwendungsfall, für den ich hier bin!

Hi @ohjames , vergib mir, ich habe Probleme, dein Beispiel zu grokeln, gibt es eine Chance, dass du es in etwas umschreiben könntest, das so funktioniert, wie es auf dem Spielplatz ist?

Irgendwelche Fortschritte dabei? Ich hatte das den ganzen Tag im Kopf, ohne mir dieses Problems bewusst zu sein, wollte es implementieren, nur um herauszufinden, dass der Compiler es nicht aufgreift. Ich habe ein Projekt, das eine bessere Protokollierungslösung gebrauchen könnte, also habe ich einen schnellen Singleton geschrieben, um ihn später zu einem vollwertigen Logger zu erweitern, den ich über einen Decorator wie Klassen anhängen wollte

<strong i="6">@loggable</strong>
class Foo { }

und ich habe den notwendigen Code dafür geschrieben

type Loggable<T> = T & { logger: Logger };

function loggable<T extends Function>(target: T): Loggable<T>
{
    Object.defineProperty(target.prototype, 'logger',
        { value: Logger.instance() });
    return <Loggable<T>> target;
}

und die Eigenschaft logger ist definitiv zur Laufzeit vorhanden, wird aber leider nicht vom Compiler abgeholt.

Ich würde gerne eine Lösung für dieses Problem sehen, zumal ein Laufzeitkonstrukt wie dieses absolut in der Lage sein sollte, zur Kompilierzeit richtig dargestellt zu werden.

Am Ende entschied ich mich für einen Immobiliendekorateur, nur um mich vorerst durchzubringen:

function logger<T>(target: T, key: string): void
{
    Object.defineProperty(target, 'logger',
        { value: Logger.instance() });
}

und Anhängen an Klassen wie

class Foo {
    <strong i="19">@logger</strong> private logger: Logger;
    ...

aber dies ist weitaus mehr Boilerplate pro Klasse, die den Logger verwendet, als ein einfacher @loggable -Klassendekorateur. Ich nehme an, ich könnte machbar wie (this as Loggable<this>).logger typisieren, aber das ist auch ziemlich weit vom Ideal entfernt, besonders nachdem ich es ein paar Mal gemacht habe. Es würde sehr schnell ermüdend werden.

Ich musste TS für eine ganze App machen, hauptsächlich weil ich https://github.com/jeffijoe/mobx-task nicht dazu bringen konnte, mit Dekorateuren zu arbeiten. Ich hoffe, dass dies bald behoben wird. 😄

Es ist sehr irritierend im Ökosystem von Angular 2, wo Dekorateure und TypeScript als Bürger erster Klasse behandelt werden. Doch in dem Moment, in dem Sie versuchen, eine Eigenschaft mit einem Decorator hinzuzufügen, sagt der TypeScript-Compiler nein. Ich hätte gedacht, dass das Angular 2-Team Interesse an diesem Thema zeigen würde.

@zajrik Sie können mit Klassen-Mixins erreichen, was Sie wollen, die seit TS 2.2 mit der richtigen Eingabe unterstützt werden:

Definieren Sie Ihr protokollierbares Mixin wie folgt:

type Constructor<T> = new(...args: any[]) => T;

interface Logger {}

// You don't strictly need this interface, type inference will determine the shape of Loggable,
// you only need it if you want to refer to Loggable in a type position.
interface Loggable {
  logger: Logger;
}

function Loggable<T extends Constructor<object>>(superclass: T) {
  return class extends superclass {
    logger: Logger;
  };
}

und dann können Sie es auf verschiedene Arten verwenden. Entweder in der extends -Klausel einer Klassendeklaration:

class Foo {
  superProperty: string;
}

class LoggableFoo extends Loggable(Foo) {
  subProperty: number;
}

TS weiß, dass Instanzen von LoggableFoo superProperty , logger und subProperty haben:

const o = new LoggableFoo();
o.superProperty; // string
o.logger; // Logger
o.subProperty; // number

Sie können auch ein Mixin als Ausdruck verwenden, der die konkrete Klasse zurückgibt, die Sie verwenden möchten:

const LoggableFoo = Loggable(Foo);

Sie _können_ auch ein Klassen-Mixin als Dekorierer verwenden, aber es hat eine etwas andere Semantik, hauptsächlich das Unterklassen Ihrer Klasse, anstatt Ihrer Klasse zu erlauben, sie zu unterteilen.

Klassen-Mixins haben mehrere Vorteile gegenüber Dekorateuren, IMO:

  1. Sie erstellen eine neue Oberklasse, sodass die Klasse, auf die Sie sie anwenden, eine Änderung hat, um sie zu überschreiben
  2. Sie geben jetzt Check ein, ohne zusätzliche Funktionen von TypeScript
  3. Sie funktionieren gut mit Typrückschluss – Sie müssen den Rückgabewert der Mixin-Funktion nicht eingeben
  4. Sie funktionieren gut mit statischen Analysen, insbesondere Jump-to-Definition - Wenn Sie zur Implementierung von logger springen, gelangen Sie zum Mixin _implementation_, nicht zur Schnittstelle.

@justinfagnani Ich hatte Mixins dafür nicht einmal in Betracht gezogen, also danke. Ich werde heute Abend einen Loggable -Mix schreiben, um die Syntax meiner Logger-Anhänge ein bisschen schöner zu machen. Die extends Mixin(SuperClass) -Route ist meine bevorzugte Route, da ich Mixins bisher seit der Veröffentlichung von TS 2.2 so verwendet habe.

Ich bevorzuge jedoch die Idee der Decorator-Syntax gegenüber Mixins, daher hoffe ich immer noch, dass für dieses spezielle Problem eine Lösung gefunden werden kann. In der Lage zu sein, Boilerplate-freie Mixins mit Decorators zu erstellen, wäre meiner Meinung nach ein großer Segen für saubereren Code.

@zajrik froh, dass der Vorschlag geholfen hat, hoffe ich

Ich verstehe immer noch nicht ganz, warum Mixins mehr Boilerplates als Decorators haben. Sie sind im syntaktischen Gewicht nahezu identisch:

Klasse Mixin:

class LoggableFoo extends Loggable(Foo) {}

vs Dekorateur:

<strong i="12">@Loggable</strong>
class LoggableFoo extends Foo {}

Meiner Meinung nach ist das Mixin viel klarer in seiner Absicht: Es generiert eine Superklasse, und Superklassen definieren Mitglieder einer Klasse, also definiert das Mixin wahrscheinlich auch Mitglieder.

Dekorateure werden für so viele Dinge verwendet, dass Sie nicht davon ausgehen können, dass sie Mitglieder definieren oder nicht. Es könnte einfach die Klasse für etwas registrieren oder ihr einige Metadaten zuordnen.

Um fair zu sein, ich denke, was @zajrik will, ist:

<strong i="7">@loggable</strong>
class Foo { }

Was unbestreitbar, wenn auch nur geringfügig, weniger Boilerplate ist.

Das heißt, ich liebe die Mixin-Lösung. Ich vergesse immer wieder, dass Mixins eine Sache sind.

Wenn Sie sich nur darum kümmern, der aktuellen Klasse Eigenschaften hinzuzufügen, dann sind Mixins im Grunde äquivalent zu Dekorateuren mit einem erheblichen Ärgernis ... wenn Ihre Klasse noch keine Oberklasse hat, müssen Sie eine leere Oberklasse erstellen, um sie zu verwenden. Auch die Syntax scheint im Allgemeinen schwerer zu sein. Außerdem ist nicht klar, ob parametrische Mixins unterstützt werden (ist extends Mixin(Class, { ... }) erlaubt).

@justinfagnani In Ihrer Liste der Gründe sind die Punkte 2-4 tatsächlich Mängel in TypeScript, keine Vorteile von Mixins. Sie gelten nicht in einer JS-Welt.

Ich denke, wir sollten uns alle darüber im Klaren sein, dass eine Mixin-basierte Lösung für das OP-Problem das Hinzufügen von zwei Klassen zur Prototypenkette beinhalten würde, von denen eine nutzlos ist. Dies spiegelt die semantischen Unterschiede zwischen Mixins und Decorators wider, Mixins geben Ihnen jedoch die Möglichkeit, die übergeordnete Klassenkette abzufangen. In 95% der Fälle ist dies jedoch nicht das, was die Leute tun möchten, sie möchten diese Klasse dekorieren. Während Mixins ihre begrenzte Verwendung haben, denke ich, dass es semantisch unangemessen ist, sie als Alternative zu Dekorateuren und Klassen höherer Ordnung zu bewerben.

Mixins sind im Grunde äquivalent zu Dekorateuren mit einem erheblichen Ärgernis ... wenn Ihre Klasse noch keine Oberklasse hat, müssen Sie eine leere Oberklasse erstellen, um sie zu verwenden

Das stimmt nicht unbedingt:

function Mixin(superclass = Object) { ... }

class Foo extends Mixin() {}

Auch die Syntax scheint im Allgemeinen schwerer zu sein.

Ich verstehe einfach nicht, wie das so ist, also müssen wir anderer Meinung sein.

Es ist auch nicht klar, ob parametrische Mixins unterstützt werden (erweitert Mixin(Class, { ... }) erlaubt).

Das sind sie sehr. Mixins sind nur Funktionen.

In Ihrer Liste der Gründe sind die Punkte 2-4 tatsächlich Mängel in TypeScript, keine Vorteile von Mixins. Sie gelten nicht in einer JS-Welt.

Dies ist ein TypeScript-Problem, daher gelten sie hier. In der JS-Welt gibt es eigentlich noch keine Dekorateure.

Ich denke, wir sollten uns alle darüber im Klaren sein, dass eine Mixin-basierte Lösung für das OP-Problem das Hinzufügen von zwei Klassen zur Prototypenkette beinhalten würde, von denen eine nutzlos ist.

Mir ist nicht klar, wo Sie zwei bekommen. Es ist eines, genau wie der Dekorateur es tun könnte, es sei denn, es wird gepatcht. Und welcher Prototyp ist nutzlos? Die Mixin-Anwendung fügt vermutlich eine Eigenschaft/Methode hinzu, die nicht nutzlos ist.

Dies spiegelt die semantischen Unterschiede zwischen Mixins und Decorators wider, Mixins geben Ihnen jedoch die Möglichkeit, die übergeordnete Klassenkette abzufangen. In 95% der Fälle ist dies jedoch nicht das, was die Leute tun möchten, sie möchten diese Klasse dekorieren.

Ich bin mir nicht sicher, ob das stimmt. Wenn Sie eine Klasse definieren, erwarten Sie normalerweise, dass sie am Ende der Vererbungshierarchie steht und die Möglichkeit hat, Methoden von Oberklassen zu überschreiben. Dekorateure müssen entweder die Klasse patchen, was zahlreiche Probleme hat, einschließlich der Nichtfunktionierung mit super() , oder sie erweitern, in diesem Fall hat die dekorierte Klasse nicht die Möglichkeit, die Erweiterung zu überschreiben. Dies kann in einigen Fällen nützlich sein, wie z. B. ein Dekorierer, der jede definierte Methode der Klasse für die Leistungs-/Debugging-Ablaufverfolgung überschreibt, aber es ist weit entfernt vom üblichen Vererbungsmodell.

Während Mixins ihre begrenzte Verwendung haben, denke ich, dass es semantisch unangemessen ist, sie als Alternative zu Dekorateuren und Klassen höherer Ordnung zu bewerben.

Wenn ein Entwickler der Prototypkette Mitglieder hinzufügen möchte, sind Mixins semantisch genau richtig. In jedem Fall, in dem ich gesehen habe, dass jemand Decorators für Mixins verwenden möchte, würde die Verwendung von Klassen-Mixins die gleiche Aufgabe erfüllen, mit der Semantik, die sie tatsächlich von Decorators erwarten, mehr Flexibilität aufgrund der Arbeitseigenschaft mit Superaufrufen und von Natürlich funktionieren sie jetzt.

Mixins sind kaum unpassend, wenn sie den Anwendungsfall direkt ansprechen.

Wenn ein Entwickler der Prototypenkette Mitglieder hinzufügen möchte

Das ist genau mein Punkt, das OP möchte der Prototypenkette nichts hinzufügen. Er möchte nur eine einzelne Klasse mutieren, und meistens, wenn Leute Decorators verwenden, haben sie nicht einmal eine andere Elternklasse als Object. Und aus irgendeinem Grund funktioniert Mixin(Object) nicht in TypeScript, also müssen Sie eine leere Dummy-Klasse hinzufügen. Jetzt haben Sie also eine Prototypkette von 2 (ohne Objekt), wenn Sie sie nicht benötigen. Außerdem entstehen nicht unerhebliche Kosten für das Hinzufügen neuer Klassen zur Prototypenkette.

Zur Syntax vergleiche Mixin1(Mixin2(Mixin3(Object, { ... }), {... }), {...}) . Die Parameter für jedes Mixin sind so weit wie möglich von der Mixin-Klasse entfernt. Die Decorator-Syntax ist deutlich besser lesbar.

Während die Decorator-Syntax per se keine Überprüfung durchführt, können Sie einfach den regulären Funktionsaufruf verwenden, um das zu erhalten, was Sie möchten:

class Logger { static instance() { return new Logger(); } }
type Loggable<T> = T & { logger: Logger };
function loggable<T, U>(target: { new (): T } & U): { new (): Loggable<T> } & U
{
    // ...
}

const Foo = loggable(class {
    x: string
});

let foo = new Foo();
foo.logger; // Logger
foo.x; // string

Es ist nur ein wenig nervig, dass Sie Ihre Klasse als const Foo = loggable(class { deklarieren müssen, aber ansonsten funktioniert alles.

@ohjames (cc @justinfagnani) Sie müssen vorsichtig sein, wenn Sie Builtins wie Object erweitern (da sie in Instanzen über den Prototyp Ihrer Unterklasse schlagen): https://github.com/Microsoft/TypeScript/wiki/FAQ #warum -erweitert-eingebaute-ins-wie-error-array-and-map-funktioniert-nicht

@nevir ja, ich habe bereits den Vorschlag von @justinfagnani ausprobiert, ein Mixin mit einem Standardparameter Object in der Vergangenheit mit TypeScript 2.2 zu verwenden, und tsc lehnt den Code ab.

@ohjames es funktioniert immer noch, Sie müssen nur im Standardfall auf den Prototyp achten (siehe diesen FAQ-Eintrag).

Allerdings ist es im Allgemeinen einfacher, sich auf das Verhalten von tslib.__extend zu verlassen, wenn null übergeben wird

Gibt es Pläne, dieses Problem im nächsten Iterationsschritt zu berücksichtigen? Die Vorteile dieser Funktion sind bei so vielen Bibliotheken extrem hoch.

Ich bin gerade auf dieses Problem gestoßen - es zwingt mich, viel unnötigen Code zu schreiben. Die Lösung dieses Problems wäre eine große Hilfe für alle Dekorator-basierten Frameworks/Bibliotheken.

@TomMarius Wie ich bereits erwähnt habe, geben Klassen, die in Decorator-Funktionen eingeschlossen sind, Check bereits korrekt ein, Sie können einfach nicht den @ -Syntaxzucker verwenden. Anstatt zu tun:

<strong i="8">@loggable</strong>
class Foo { }

Sie müssen nur Folgendes tun:

const Foo = loggable(class { });

Sie können sogar eine Reihe von Decorator-Funktionen zusammenstellen, bevor Sie eine Klasse darin verpacken. Es ist zwar wertvoll, den Syntaxzucker richtig zum Laufen zu bringen, aber es scheint nicht so, als ob dies ein so großer Schmerzpunkt sein sollte, wie die Dinge sind.

@masaeedu Das Problem ist wirklich nicht die externe, sondern die interne Typunterstützung. Die Eigenschaften, die der Dekorateur innerhalb der Klasse selbst hinzufügt, ohne Kompilierungsfehler verwenden zu können, ist zumindest für mich das gewünschte Ergebnis. Das von Ihnen bereitgestellte Beispiel würde Foo nur den protokollierbaren Typ geben, aber den Typ nicht für die Klassendefinition selbst bereitstellen.

@zajrik Ein Decorator gibt eine neue Klasse von einer ursprünglichen Klasse zurück, selbst wenn Sie die eingebaute @ -Syntax verwenden. Offensichtlich erzwingt JS keine Reinheit, daher steht es Ihnen frei, die ursprüngliche Klasse, die Sie übergeben haben, zu mutieren, aber dies widerspricht der idiomatischen Verwendung des Decorator-Konzepts. Wenn Sie die Funktionalität, die Sie über Dekoratoren hinzufügen, eng an die Klasseninterna koppeln, können sie auch interne Eigenschaften sein.

Können Sie mir ein Beispiel für einen Anwendungsfall für eine klassenintern konsumierende API geben, die zu einem späteren Zeitpunkt über Decorators hinzugefügt wird?

Das obige Logger-Beispiel ist ein gutes Beispiel für ein allgemeines _want_, um die Interna der dekorierten Klasse manipulieren zu können. (Und ist Leuten bekannt, die aus anderen Sprachen mit Klassendekoration kommen; wie Python )

Trotzdem scheint der Klassen-Mixin-Vorschlag von @justinfagnani eine gute Alternative für diesen Fall zu sein

Wenn Sie in der Lage sein möchten, die Interna einer Klasse zu definieren, besteht der strukturierte Weg, dies zu tun, nicht darin, die Klasse zu patchen oder eine neue Unterklasse zu definieren, über die TypeScript im Kontext der Klasse nur schwer nachdenken kann selbst, sondern um entweder nur Dinge in der Klasse selbst zu definieren oder eine neue Oberklasse mit den erforderlichen Eigenschaften zu erstellen, über die TypeScript nachdenken kann.

Dekorateure sollten die Form einer Klasse wirklich nicht auf eine Weise ändern, die für die Klasse oder die meisten Verbraucher sichtbar ist. @masaeedu ist gleich hier.

Während das, was Sie sagen, wahr ist, ist TypeScript nicht dazu da, saubere Codierungspraktiken zu erzwingen, sondern um JavaScript-Code korrekt einzugeben, und es schlägt in diesem Fall fehl.

@masaeedu Was @zajrik gesagt hat. Ich habe einen Dekorateur, der einen "Onlinedienst" deklariert, der einer Klasse eine Reihe von Eigenschaften hinzufügt, die dann in der Klasse verwendet werden. Das Unterklassen oder Implementieren einer Schnittstelle ist aufgrund fehlender Metadaten und der Durchsetzung von Einschränkungen keine Option (wenn Sie keine Codeduplizierung anstreben).

@TomMarius Mein Punkt ist, dass die Typprüfung korrekt ist. Wenn Sie eine Decorator-Funktion auf eine Klasse anwenden, wird die Klasse in keiner Weise geändert. Eine neue Klasse wird durch eine Transformation der ursprünglichen Klasse erzeugt, und nur diese neue Klasse unterstützt garantiert die von der Decorator-Funktion eingeführte API.

Ich weiß nicht, was "fehlende Metadaten und Durchsetzung von Einschränkungen" bedeutet (vielleicht würde ein konkretes Beispiel helfen), aber wenn Ihre Klasse explizit auf die vom Dekorateur eingeführte API angewiesen ist, sollte sie sie einfach direkt über das von @justinfagnani gezeigte Mixin-Muster ableiten , oder injizieren Sie es durch den Konstruktor oder so. Der Nutzen von Dekoratoren besteht darin, dass sie es ermöglichen, dass Klassen, die für Änderungen geschlossen sind, zugunsten von Code erweitert werden können, der diese Klassen verbraucht . Wenn Sie die Klasse selbst definieren können, verwenden Sie einfach extends .

@masaeedu Wenn Sie beispielsweise eine Art RPC-Bibliothek entwickeln und den Benutzer zwingen möchten, nur asynchrone Methoden zu schreiben, zwingt Sie der vererbungsbasierte Ansatz, Code zu duplizieren (oder ich habe den richtigen Weg nicht gefunden , vielleicht - ich würde mich freuen, wenn du mir sagst, ob du weißt wie).

Vererbungsbasierter Ansatz
Bedeutung: export abstract class Service<T extends { [P in keyof T]: () => Promise<IResult>}> { protected someMethod(): Promise<void> { return Promise.reject(""); } }
Verbrauch: export default class MyService extends Service<MyService> { async foo() { return this.someMethod(); } }

Decorator-basierter Ansatz
Bedeutung: export function service<T>(target: { new (): T & { [P in keyof T]: () => Promise<IResult> } }) { target.someMethod = function () { return Promise.reject(""); }; return target; }
Verbrauch: <strong i="17">@service</strong> export default class { async foo() { return this.someMethod(); } }

Sie können die Code-Duplizierung im Beispiel des vererbungsbasierten Ansatzes deutlich sehen. Mir und meinen Benutzern ist es oft passiert, dass sie vergessen haben, den Typparameter zu ändern, wenn sie die Klasse kopiert und eingefügt haben oder angefangen haben, "any" als Typparameter zu verwenden, und die Bibliothek für sie nicht mehr funktioniert. der decorator-basierte Ansatz ist viel entwicklerfreundlicher.

Danach gibt es noch ein weiteres Problem mit dem vererbungsbasierten Ansatz: Reflection-Metadaten fehlen jetzt, sodass Sie Code noch mehr duplizieren müssen, weil Sie sowieso den service -Dekorator einführen müssen. Die Verwendung ist jetzt: <strong i="22">@service</strong> export default class MyService extends Service<MyService> { async foo() { return this.someMethod(); } } , und das ist einfach nur unfreundlich, nicht nur eine kleine Unannehmlichkeit.

Sie sind richtig, dass die Änderung semantisch erfolgt, nachdem die Klasse definiert wurde, aber es gibt keine Möglichkeit, die nicht dekorierte Klasse zu instanziieren, also gibt es keinen Grund, die Mutation der Klasse nicht richtig zu unterstützen, außer dass sie manchmal unsauberen Code zulässt (aber manchmal zum besseren Wohl). Denken Sie daran, dass JavaScript immer noch auf Prototypen basiert, die Klassensyntax ist nur ein Zucker, um es zu vertuschen. Der Prototyp ist änderbar und kann vom Dekorateur stummgeschaltet werden, und er sollte korrekt eingegeben werden.

Wenn Sie eine Decorator-Funktion auf eine Klasse anwenden, wird die Klasse in keiner Weise geändert.

Nicht wahr, wenn Sie eine Decorator-Funktion auf eine Klasse anwenden, kann die Klasse in irgendeiner Weise geändert werden. Ob du willst oder nicht.

@TomMarius Sie versuchen, die Schlussfolgerung auszunutzen, um einen Vertrag durchzusetzen, was für das hier geführte Argument völlig irrelevant ist. Sie sollten nur Folgendes tun:

function service<T>(target: { new (): T & {[P in keyof T]: () => Promise<any> } }) { return target };

// Does not type check
const MyWrongService = service(class {
    foo() { return ""; }
})

// Type checks
const MyRightService = service(class {
    async foo() { return ""; }
})

Es ist absolut nicht erforderlich, dass die Interna der Klasse sich der Dekorationsfunktion bewusst sind.

@masaeedu Das war nicht mein Punkt. Der Service-Decorator/die Service-Klasse führt einige neue Eigenschaften ein, und diese stehen der zu verwendenden Klasse immer zur Verfügung, aber das Typsystem gibt dies nicht korrekt wieder. Sie sagten, ich solle dafür Vererbung verwenden, also habe ich Ihnen gezeigt, warum ich das nicht kann/will.

Ich habe das Beispiel bearbeitet, um es klarer zu machen.

@masaeedu Übrigens ist die Aussage "Ein Dekorateur gibt eine neue Klasse von einer Originalklasse zurück" falsch - jeder Dekorateur, den wir beide hier gezeigt haben, gibt eine mutierte oder direkt die Originalklasse zurück, niemals eine neue.

@TomMarius In Ihrem Kommentar wurde ein Problem erwähnt, bei dem "den Benutzer gezwungen wird, nur asynchrone Methoden zu schreiben". Dies ist das Problem, das ich in meinem Kommentar ansprechen wollte. Das Erzwingen, dass der Benutzer den von Ihnen erwarteten Vertrag befolgt hat, sollte überall dort erfolgen, wo der Code des Benutzers an die Bibliothek zurückgegeben wird, und hat nichts mit der Diskussion darüber zu tun, ob Dekorateure die Typform ändern sollten, die den Interna der Klasse präsentiert wird. Das orthogonale Problem der Bereitstellung einer API für den Code des Benutzers kann mit standardmäßigen Vererbungs- oder Kompositionsansätzen gelöst werden.

@ohjames Die Klasse wird nicht nur durch Anwenden eines Dekorateurs geändert. JS erzwingt keine Reinheit, daher kann offensichtlich jede Anweisung irgendwo in Ihrem Code etwas anderes ändern, aber das ist für diese Feature-Diskussion irrelevant. Selbst wenn die Funktion implementiert ist, hilft Ihnen TypeScript nicht dabei, willkürliche strukturelle Änderungen innerhalb von Funktionskörpern zu verfolgen.

@masaeedu Sie pflücken Teile, aber ich spreche über das Gesamtbild. Bitte überprüfen Sie alle meine Kommentare in diesem Thread - der Punkt liegt nicht in den einzelnen Problemen, sondern in jedem Problem, das gleichzeitig passiert. Ich glaube, ich habe das Problem mit dem vererbungsbasierten Ansatz gut erklärt - viele, viele Codeduplizierungen.

Aus Gründen der Klarheit geht es mir hier nicht um "sauberen Code". Das Problem ist eines der Praktikabilität; Sie brauchen keine massiven Änderungen am Typsystem, wenn Sie @foo genauso behandeln wie eine Funktionsanwendung. Wenn Sie die Dose der Würmer öffnen, um zu versuchen, Typinformationen in das Argument einer Funktion aus ihrem Rückgabetyp einzuführen, während Sie gleichzeitig mit der Typinferenz und all den anderen magischen Bestien interagieren, die in verschiedenen Ecken des TypeScript-Typsystems zu finden sind, fühle ich mich Dies wird zu einem großen Hindernis für neue Funktionen, ähnlich wie es jetzt die Überladung ist.

@TomMarius Ihr erster Kommentar in diesem Thread bezieht sich auf sauberen Code, der nicht relevant ist. Der nächste Kommentar bezieht sich auf dieses Onlinedienstkonzept, für das Sie den Beispielcode bereitgestellt haben. Die Hauptbeschwerde vom ersten bis zum vierten Absatz betrifft die Fehleranfälligkeit bei der Verwendung MyService extends Service<MyService> . Ich habe versucht, ein Beispiel zu zeigen, wie man damit umgehen kann.

Ich habe es mir noch einmal angesehen, und ich kann in diesem Beispiel wirklich nichts erkennen, das veranschaulicht, warum die Mitglieder der dekorierten Klasse den Dekorateur kennen müssten. Was hat es mit diesen neuen Eigenschaften auf sich, die Sie dem Benutzer zur Verfügung stellen, die mit der Standardvererbung nicht erreicht werden können? Ich habe nicht mit Reflexion gearbeitet, also habe ich das irgendwie überflogen, Entschuldigung.

@masaeedu Ich kann es mit Vererbung erreichen, aber die Vererbung zwingt mich/meine Benutzer, Code massiv zu duplizieren, also hätte ich gerne einen anderen Weg - und das tue ich, aber das Typsystem kann die Realität nicht korrekt widerspiegeln.

Der Punkt ist der korrekte Typ von <strong i="7">@service</strong> class X { } , wobei service als <T>(target: T) => T & IService deklariert wird, ist nicht X , sondern X & IService ; und das Problem ist, dass es in Wirklichkeit sogar innerhalb der Klasse wahr ist – obwohl es semantisch nicht wahr ist.

Ein weiterer großer Schmerzpunkt, den dieses Problem verursacht, ist, dass, wenn Sie eine Reihe von Dekorateuren haben, die jeweils einige Einschränkungen haben, das Typsystem denkt, dass das Ziel immer die ursprüngliche Klasse ist, nicht die dekorierte, und daher die Einschränkung nutzlos ist.

Ich kann es mit Vererbung erreichen, aber die Vererbung zwingt mich/meine Benutzer, Code massiv zu duplizieren, also hätte ich gerne einen anderen Weg.

Das ist der Teil, den ich nicht verstehe. Ihre Benutzer müssen IService implementieren, und Sie möchten sicherstellen, dass TheirService implements IService auch einem anderen Vertrag { [P in keyof blablablah] } gehorcht, und vielleicht möchten Sie auch, dass sie einen { potato: Potato } haben auf ihren Dienst. All dies ist einfach zu bewerkstelligen, ohne dass die Kursteilnehmer @service :

import { serviceDecorator, BaseService } from 'library';

// Right now
const MyService = serviceDecorator(class extends BaseService {
    async foo(): { return ""; }
})

const MyBrokenService1 = serviceDecorator(class extends BaseService {
    foo(): { return; } // Whoops, forgot async! Not assignable
});

const MyBrokenService2 = serviceDecorator(class { // Whoops, forgot to extend BaseService! Not assignable
    async foo(): { return; } 
});

// Once #4881 lands
<strong i="13">@serviceDecorator</strong>
class MyService extends BaseService {
    async foo(): { return ""; }
}

In keinem Fall gibt es einen riesigen Gewinn an Prägnanz, der nur erreicht werden kann, indem der Rückgabetyp von serviceDecorator als der Typ von this bis foo präsentiert wird. Und was noch wichtiger ist, wie schlagen Sie vor, dafür eine vernünftige Schreibstrategie zu finden? Der Rückgabetyp von serviceDecorator wird basierend auf dem Typ der Klasse, die Sie dekorieren, abgeleitet, die nun wiederum als Rückgabetyp des Dekorators typisiert wird ...

Hallo @masaeedu , die Prägnanz wird besonders wertvoll, wenn Sie mehr als eine haben.

@Component({ /** component args **/})
@Authorized({/** roles **/)
<strong i="7">@HasUndoContext</strong>
class MyComponent  {
  // do stuff with undo context, component methods etc
}

Dieser Workaround ist jedoch nur eine Alternative zu Klassendekorateuren. Für Methodendekorateure gibt es derzeit keine Lösungen und blockiert so viele nette Implementierungen. Dekorateure sind in Phase 2 im Vorschlag – https://github.com/tc39/proposal-decorators . Es gab jedoch viele Fälle, in denen die Implementierung viel früher erfolgte. Ich denke, besonders Dekoratoren sind einer dieser wichtigen Bausteine, die wirklich wichtig waren, da sie bereits in so vielen Frameworks verwendet wurden und eine sehr einfache Version bereits in babel/ts implementiert ist. Wenn dieses Problem implementiert werden könnte, würde es seinen experimentellen Status bis zur offiziellen Veröffentlichung nicht verlieren. Aber das ist "experimentell" für.

@pietschy Ja, es wäre schön, wenn der @ -Syntaxzucker mit der Typprüfung richtig funktioniert. Im Moment können Sie die Funktionskomposition verwenden, um eine einigermaßen ähnliche Prägnanz zu erhalten:

const decorator = compose(
    Component({ /** component args **/ }),
    Authorized({ /** roles **/ })
    HasUndoContext
);

const MyComponent = decorator(class {
});

In der vorhergehenden Diskussion ging es darum, ob es eine gute Idee ist, eine Art inverse Typisierung durchzuführen, bei der der Rückgabetyp des Dekorators den Klassenmitgliedern als der Typ von this präsentiert wird.

Hallo @masaeedu , ja, ich habe den Kontext der Diskussion verstanden, daher das // do stuff with undo context, component methods etc . Beifall

Mixins wirklich einfacher machen müssen.

Typoskript (Javascript) unterstützt keine Mehrfachvererbung, daher müssen wir Mixins oder Traits verwenden.
Und jetzt verschwende ich so viel Zeit, besonders wenn ich etwas rekonstruiere.
Und ich muss überall eine Schnittstelle mit ihrer "leeren Implementierung" kopieren und einfügen. (#371)

--
Ich denke nicht, dass der Rückkehrtyp des Dekorateurs in der Klasse präsentiert werden sollte.
weil: .... ähm. Ich weiß nicht, wie ich es beschreiben soll, entschuldige meine schlechten Englischkenntnisse ... ( 🤔 kann ein Foto ohne Fotorahmen existieren? ) Dieser Job kostet interface .

Ich werde meine +1 zu diesem hinzufügen! Ich würde es gerne bald sehen.

@pietschy Wenn die Klasse von Mitgliedern abhängt, die von den Dekorateuren hinzugefügt wurden, sollte sie das Ergebnis der Dekorateure erweitern, nicht umgekehrt. Du solltest tun:

const decorator = compose(
    Component({ /** component args **/ }),
    Authorized({ /** roles **/ })
    HasUndoContext
);

class MyComponent extends decorator(class { }) {
    // do stuff with undo context, component methods etc
};

Die Alternative besteht darin, dass das Typsystem in einer Art Schleife arbeitet, in der das Argument der Decorator-Funktion kontextuell durch ihren Rückgabetyp typisiert wird, der aus ihrem Argument abgeleitet wird, das wiederum durch ihren Rückgabetyp kontextuell typisiert wird usw. Wir brauchen noch a konkreten Vorschlag, wie das funktionieren soll, sondern wartet nicht nur auf die Umsetzung.

Hallo @masaeedu , ich bin verwirrt, warum ich meine Dekorateure zusammenstellen und auf eine Basisklasse anwenden müsste. Soweit ich weiß, wurde der Prototyp bereits modifiziert, wenn ein Landcode des Benutzers ausgeführt wird. Z.B

function pressable<T extends {new(...args:any[]):{}}>(constructor:T) {
    return class extends constructor {
        pressMe() {
            console.log('how depressing');
        }
    }
}

<strong i="7">@pressable</strong>
class UserLandClass {
    constructor() {    
        this['pressMe'](); // this method exists, please let me use code completion.
    }
}

console.log(new UserLandClass());

Wenn also von Dekorateuren definierte Methoden bereits existieren und legitim aufgerufen werden können, wäre es schön, wenn Typoskript dies widerspiegeln würde.

Der Wunsch ist, dass Typoskript dies widerspiegelt, ohne Problemumgehungen aufzuerlegen. Wenn es andere gibt
Dinge, die Dekorateure können, die nicht modelliert werden können, dann wäre es zumindest schön, wenn dies der Fall wäre
Szenario in irgendeiner Form unterstützt wurde.

Dieser Thread ist voll von Meinungen darüber, was Dekorateure tun sollten , wie sie verwendet werden sollten , wie sie nicht verwendet werden sollten usw. bis zum Erbrechen .

Das ist, was Dekorateure eigentlich tun:

function addMethod(Class) : any {
    return class extends Class {
        hello(){}
    };
}

<strong i="13">@addMethod</strong>
class Foo{
    originalMethod(){}
}

was wird, wenn Sie auf esnext abzielen

var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};

function addMethod(Class) {
    return class extends Class {
        hello() {
        }
    };
}
let Foo = class Foo {
    originalMethod() { }
};
Foo = __decorate([
    addMethod
], Foo);

wobei __decorate jeden Decorator von unten nach oben anwendet, wobei jeder die Option hat, eine ganz neue Klasse zurückzugeben.

Ich verstehe, dass es sehr schwierig sein kann, das Typsystem dazu zu bringen, dies zu unterstützen und anzuerkennen, und ich verstehe, dass es sehr wohl einige Zeit dauern kann, dies zu unterstützen, aber können wir uns nicht alle darauf einigen, dass das aktuelle Verhalten aus dem ursprünglichen Codebeispiel oben damit begonnen hat? Thread, ist einfach falsch?

Unabhängig von der Meinung eines Entwicklers zu Dekorateuren oder Mixins oder funktionaler Komposition usw. usw. scheint dies ein ziemlich klarer Fehler zu sein.


Darf ich höflich nachfragen, ob dies zufällig für eine zukünftige Veröffentlichung geplant ist?

TypeScript ist einfach fantastisch und ich liebe es; Dies scheint jedoch eines der wenigen (einzigen?) Teile zu sein, das einfach kaputt ist, und ich freue mich nur darauf, es repariert zu sehen :)

@arackaf Es ist gut bekannt, was Dekorateure tatsächlich tun, und vorausgesetzt, Sie interessieren sich nicht für die Typen, die TypeScripts Emit bereits unterstützt. Die gesamte Debatte in dieser Ausgabe dreht sich darum, wie dekorierte Klassen im Typensystem dargestellt werden sollten.

Ich stimme zu, dass new Foo().foo ein Typfehler ist, und ein Fehler, der leicht behoben werden kann. Ich stimme nicht zu, dass es sich return this.foo; um einen Typfehler handelt. Wenn überhaupt, fragen Sie nach einer Funktion im Typensystem, die bisher noch niemand in dieser Ausgabe spezifiziert hat. Wenn Sie einen Mechanismus im Sinn haben, durch den der Typ von this durch einen Dekorator transformiert werden soll, der auf die enthaltende Klasse angewendet wird, müssen Sie diesen Mechanismus explizit vorschlagen .

Ein solcher Mechanismus ist nicht so trivial, wie Sie vielleicht erwarten, da der Rückgabetyp des Dekorators in fast allen Fällen von genau dem Typ abgeleitet wird, den Sie mithilfe des Rückgabetyps des Dekorators transformieren möchten. Wenn der Dekorateur addMethod eine Klasse vom Typ new () => T nimmt und new() => T & { hello(): void } erzeugt, macht es keinen Sinn vorzuschlagen, dass T dann T & { hello(): void } sein sollte super.hello verweisen?

Dies ist besonders relevant, weil ich nicht darauf beschränkt bin, return class extends ClassIWasPassed { ... } im Körper des Dekorateurs zu machen, ich kann tun, was ich will; Subtraktionstypen, abgebildete Typen, Vereinigungen, sie sind alle Freiwild. Jeder resultierende Typ muss mit dieser von Ihnen vorgeschlagenen Inferenzschleife gut funktionieren. Sagen Sie mir bitte zur Veranschaulichung des Problems, was in diesem Beispiel passieren soll:

type Constructor<T> = new (...args: any[]) => T
type Greeter = { hello(): string }

function logger<T extends Constructor<Greeter>>(Class: T) {
    return class {
        hello() {
            console.log(new Class().hello()) // Do I get a type error here? After all, the signature of Class is { hello: () => void }
        } 
    };
}

// Or should I get the error here? Foo does not conform to { hello(): string }
<strong i="22">@logger</strong>
class Foo {
    hello() { return "foo"; }
}

Sie können das Problem nicht einfach als "knifflig" wegwinken, jemand muss tatsächlich vorschlagen, wie dies funktionieren soll.

Kein Fehler - Sie übergeben Logger eine Klasse, die Greeter entspricht; Foo wird dann jedoch vom Dekorateur zu etwas völlig anderem mutiert, was Greeter nicht mehr erweitert. Daher sollte es für den Logger-Dekorateur gültig sein, Foo als Greeter zu behandeln, aber für alle anderen ungültig, da Foo vom Dekorateur etwas völlig anderem zugewiesen wird.

Ich bin mir sicher, dies umzusetzen , wäre sehr schwierig. Ist eine vernünftige Teilmenge für die häufigsten Fälle wie den oben in diesem Thread aufgeführten möglich, wobei Problemumgehungen wie das Zusammenführen von Schnittstellen Fallbacks für knifflige Randfälle wie diesen sind?

@arackaf

Foo wird dann jedoch vom Dekorateur zu etwas völlig anderem mutiert

Es gibt keine genaue Definition dafür, was Foo ist. Denken Sie daran, dass unser einziger Streitpunkt darin besteht, ob die Mitglieder von $#$1 Foo #$ auf die von Decorator eingeführten Mitglieder von this zugreifen können (und daher als Teil der öffentlichen API zurückkehren können). Was Foo ist, wird durch das definiert, was der Decorator zurückgibt, und was der Decorator zurückgibt, wird durch das definiert, was Foo ist. Sie sind untrennbar miteinander verbunden.

Ich bin sicher, dies umzusetzen wäre bösartig schwierig

Es ist noch verfrüht, über die Komplexität der Implementierung zu sprechen, wenn wir noch nicht einmal einen soliden Vorschlag haben, wie das Feature funktionieren soll. Ich entschuldige mich dafür, dass ich mich darüber aufrege, aber wir brauchen wirklich jemanden, der einen konkreten Mechanismus dafür vorschlägt, "was mit this passiert, wenn ich eine solche Folge von Dekorateuren auf eine solche Klasse anwende". Wir können dann verschiedene TypeScript-Konzepte in verschiedene Teile einbauen und sehen, ob das, was dabei herauskommt, sinnvoll ist. Erst dann macht es Sinn, die Umsetzungskomplexität zu diskutieren.

Ist die aktuelle Decorator-Ausgabe, die ich oben eingefügt habe, nicht spezifikationskonform? Ich nahm an, dass es von dem war, was ich gesehen habe.

Vorausgesetzt, Foo hat bei jedem Schritt eine ziemlich genaue Bedeutung. Ich würde erwarten, dass TS mir erlaubt, this (innerhalb von Foo-Methoden und auf Instanzen von Foo) zu verwenden, basierend auf dem, was der letzte Dekorateur zurückgibt, wobei TS mich dazu zwingt, Typanmerkungen nach Bedarf hinzuzufügen, wenn der Dekorateur Funktionen sind nicht ausreichend analysierbar.

@arackaf Es gibt keine "Schritte des Weges"; Wir interessieren uns nur für den endgültigen Typ von this für einen bestimmten unveränderlichen Codeausschnitt. Sie müssen zumindest grob beschreiben, was der Typ von this für Mitglieder einer Klasse X sein sollte, die mit Dekorationsfunktionen f1...fn ausgestattet ist der Typsignaturen von X und f1...fn . Sie können so viele Schritte im Prozess haben, wie Sie möchten. Bisher hat das noch niemand gemacht. Ich habe vermutet, dass die Leute meinen, dass der Rückgabetyp des Dekorateurs als Typ von this dargestellt werden sollte, aber soweit ich weiß, könnte ich völlig daneben liegen.

Wenn Ihr Vorschlag darin besteht, die Typen mechanisch widerzuspiegeln, was mit den Werten in der transpilierten Ausgabe passiert, erhalten Sie am Ende das, was ich vorschlage, anstatt das, was Sie vorschlagen: dh new Foo().hello() ist in Ordnung, aber this.hello() ist es nicht. Zu keinem Zeitpunkt in diesem Beispiel erhält die ursprüngliche Klasse, die Sie dekorieren, eine hello -Methode. Nur das Ergebnis von __decorate([addMethod], Foo) (das dann Foo zugewiesen wird) hat eine hello Methode.

Ich habe vermutet, dass die Leute meinen, dass der Rückgabetyp des Dekorateurs als Typ von diesem dargestellt werden sollte

Oh, Entschuldigung, ja, das stimmt. Genau das. Punkt. Denn das tun die Dekorateure. Wenn der letzte Dekorateur in der Reihe eine völlig neue Klasse zurückgibt, dann IST das, was Foo ist.

Mit anderen Worten:

<strong i="9">@c</strong>
<strong i="10">@b</strong>
<strong i="11">@a</strong>
class Foo { 
}

Foo ist was auch immer in der Welt c sagt. Wenn c eine Funktion ist, die dann any zurückgibt, weiß ich nicht - vielleicht einfach auf das ursprüngliche Foo ? Das scheint ein vernünftiger, abwärtskompatibler Ansatz zu sein.

aber wenn c einen neuen Typ X $ zurückgibt, dann würde ich absolut erwarten, dass Foo das respektiert.

Übersehe ich etwas?


weiter klären, ggf

class X { 
    hello() {}
    world() {}
}
function c(cl : any) : X {  // or should it be typeof X ?????
    //...
}

<strong i="25">@c</strong>
<strong i="26">@b</strong>
<strong i="27">@a</strong>
class Foo { 
    sorry() {}
}

new Foo().hello(); //perfectly valid
new Foo().sorry(); //ERROR 

Übersehe ich etwas?

@arackaf Ja, was diesem naiven Ansatz fehlt, ist, dass ein Dekorateur beliebige Typen zurückgeben kann, ohne Einschränkung, dass das Ergebnis mit dem Typ von Foo kompatibel ist.

Damit kann man jede Menge Absurditäten produzieren. Nehmen wir an, ich halte meinen Einwand gegen die Zirkularität der Eingabe von this als Ergebnis von c zurück, was durch den Typ von this bestimmt wird, der durch das Ergebnis von c bestimmt wird

type Constructor<T> = new (...args: any[]) => T
type Greeter = { hello(): string }

function logger<T extends Constructor<Greeter>>(Class: T) {
    return class {
        hello() {
            console.log(new Class().hello())
        }
    };
}


<strong i="15">@logger</strong>
class Foo {
    foo() { return "bar" }
    // Whoops, `this` is `{ hello(): void }`, it has no `foo` method
    hello() { return this.foo(); }
}

In diesem Fall müssen Sie entweder einen Fehler für völlig harmlosen Code erzeugen oder die Behauptung anpassen, dass this genau identisch mit dem Rückgabetyp des Dekorators ist.

Ich bin mir nicht sicher, ob ich das Problem sehe. @logger hat sich dafür entschieden, eine brandneue Klasse zurückzugeben, ohne irgendeine foo -Methode und mit einer völlig neuen hello() -Methode, die zufällig auf das Original verweist, das jetzt nicht mehr erreichbar ist Foo .

new Foo().foo()

ist nicht länger gültig; es wird einen Laufzeitfehler erzeugen. Ich sage nur, dass es auch einen Kompilierzeitfehler erzeugen sollte.

Das heißt, wenn die statische Analyse all dessen zu schwierig ist, wäre es meiner Meinung nach absolut vernünftig, uns zu zwingen, explizite Typanmerkungen zu logger hinzuzufügen, um genau anzugeben, was zurückgegeben wird. Und wenn keine solche Typanmerkung vorhanden ist, würde ich sagen, dass einfach angenommen wird, dass Foo zurückgegeben wird. Das sollte es abwärtskompatibel halten.

@arackaf Es gibt kein Problem mit dem Code in Bezug auf Typisierung oder Laufzeitauswertung. Ich kann new Foo().hello() aufrufen, wodurch intern hello der dekorierten Klasse aufgerufen wird, die wiederum bar der dekorierten Klasse aufruft. Es ist kein Fehler für mich, bar innerhalb der ursprünglichen Klasse aufzurufen.

Sie können es selbst ausprobieren, indem Sie dieses vollständige Beispiel im Playground ausführen:

// Code from previous snippet...

const LoggerFoo = logger(Foo)
new LoggerFoo().hello()

Sicher - aber ich sagte, es sei ein Fehler beim Aufrufen

new Foo().foo()

Es gibt kein Problem mit dem Code in Bezug auf Typisierung oder Laufzeitauswertung. Ich kann new Foo().hello() aufrufen, was intern das hallo der dekorierten Klasse aufruft, das wiederum die Bar der dekorierten Klasse aufruft

Aber es sollte ein Fehler sein zu sagen

let s : string = new Foo().hello();

da die hello-Methode von Foo jetzt void zurückgibt, je nach Klasse, die Logger zurückgibt.

Sicher - aber ich sagte, es sei ein Fehler, new Foo().foo() aufzurufen

@arackaf Aber das spielt keine Rolle. Ich habe new Foo().foo() nicht aufgerufen. Ich habe this.foo() aufgerufen und einen Typfehler erhalten, obwohl mein Code zur Laufzeit einwandfrei funktioniert .

Aber es sollte ein Fehler sein, let s : string = new Foo().hello() zu sagen

Auch dies ist irrelevant. Ich sage nicht, dass der letzte Typ von Foo.prototype.hello () => string sein sollte (ich stimme zu, dass es () => void sein sollte). Ich beschwere mich über die fehlerhafte gültige Invokation this.bar() , weil Sie einen Typ chirurgisch verpflanzt haben, wo es unsinnig ist, ihn zu verpflanzen.

Hier sind zwei Foo's. Wenn du sagst

class Foo { 
}

Foo ist eine unveränderliche Bindung INNERHALB der Klasse und eine veränderliche Bindung außerhalb der Klasse. Das funktioniert also perfekt, wie Sie in einem jsbin überprüfen können

class Foo { 
  static sMethod(){
    alert('works');
  }
  hello(){ 
    Foo.sMethod();
  }
}

let F = Foo;

Foo = null;

new F().hello();

Ihr obiges Beispiel macht ähnliche Dinge; Es erfasst einen Verweis auf die ursprüngliche Klasse, bevor diese äußere Bindung mutiert wird. Ich bin mir immer noch nicht sicher, worauf du hinaus willst.

this.foo(); ist vollkommen gültig, und ich würde keinen Tippfehler erwarten (ich würde auch nicht die TS-Leute beschuldigen, wenn ich eine Referenz bräuchte, da ich sicher bin, dass es schwierig wäre, dies nachzuvollziehen).

this.foo(); ist vollkommen gültig, und ich würde keinen Typfehler erwarten

Gut, also stimmen wir zu, aber das bedeutet, dass Sie jetzt den Vorschlag qualifizieren oder ablehnen müssen, dass this der Instanztyp dessen ist, was der Dekorateur zurückgibt. Wenn Sie der Meinung sind, dass es sich nicht um einen Tippfehler handeln sollte, was sollte in meinem Beispiel this anstelle von { hello(): void } sein?

this hängt davon ab, was instanziiert wurde.

<strong i="7">@c</strong>
class Foo{
}

new Foo(). // <---- this is based on whatever c returned 

function c(Cl){
    new Cl().  // <----- this is an object whose prototype is the original Foo's prototype
                   // but for TS's purpose, for type errors, it'd depend on how Cl is typed
}

Können wir bitte bei einem konkreten Beispiel bleiben? Das würde mir vieles verständlicher machen. Im folgenden Ausschnitt:

type Constructor<T> = new (...args: any[]) => T
type Greeter = { hello(): string }

function logger<T extends Constructor<Greeter>>(Class: T) {
    return class {
        hello() {
            console.log(new Class().hello())
        }
    };
}

<strong i="6">@logger</strong>
class Foo {
    foo() { return "bar" }
    hello() { return this.foo(); } /// <------
}

Was ist die Art von this ? Wenn es { hello(): void } ist, erhalte ich einen Typfehler, weil foo kein Mitglied von { hello(): void } ist. Wenn es nicht { hello(): void } ist, dann ist this nicht einfach der Instanztyp des Rückgabetyps des Dekorators, und Sie müssen erklären, welche alternative Logik Sie verwenden, um zum Typ von this zu gelangen.

BEARBEITEN: Ich habe vergessen, den Dekorateur auf Foo hinzuzufügen. Fest.

this , wo Sie die Pfeile haben, ist natürlich eine Instanz des Originals Foo . Es liegt kein Tippfehler vor.

Ah - jetzt verstehe ich Ihren Punkt; aber ich sehe immer noch nicht, wo das Problem ist. this.foo() INNERHALB des ursprünglichen Foo ist kein Typfehler - das gilt für die (jetzt nicht erreichbare) Klasse, die früher an den Bezeichner Foo gebunden war.

Es ist eine eigenartige, lustige Trivia, aber ich verstehe nicht, warum dies TS daran hindern sollte, mit mutierenden Klassendekorateuren umzugehen.

@arackaf Du beantwortest die Frage nicht. Was genau ist der Typ von this ? Sie können nicht endlos im Kreis antworten: „ this ist Foo und Foo ist this “. Welche Mitglieder hat this ? Wenn es außer hello(): void noch andere Mitglieder hat, welche Logik wird verwendet, um sie zu bestimmen?

Wenn Sie sagen " this.foo() INNERHALB des ursprünglichen Foo ist kein Typfehler", müssen Sie immer noch die Frage beantworten: Was ist der strukturelle Typ von this , sodass es sich nicht um einen Typfehler handelt? machen this.foo() ?

Außerdem ist die ursprüngliche Klasse nicht "unerreichbar". Jede in diesem Code-Snippet definierte Funktion wird zur Laufzeit aktiv ausgeführt, und alles funktioniert reibungslos. Bitte führen Sie den Playground-Link aus, den ich bereitgestellt habe, und sehen Sie sich die Konsole an. Der Decorator gibt eine neue Klasse zurück, in der die hello -Methode an die hello -Methode der dekorierten Klasse delegiert, die wiederum an die foo -Methode der dekorierten Klasse delegiert.

Es ist eine eigenartige, lustige Trivia, aber ich verstehe nicht, warum dies TS daran hindern sollte, mit mutierenden Klassendekorateuren umzugehen.

Es gibt keine "Trivia" im Typensystem. Sie werden keinen TSC-1234-Typfehler "Naughty Boy, you can't do that" erhalten, da der Anwendungsfall zu nischenhaft ist. Wenn ein Feature dazu führt, dass ganz normaler Code auf überraschende Weise kaputt geht, muss das Feature überdacht werden.

Sie werden keinen TSC-1234-Typfehler "Naughty Boy, you can't do that" erhalten, da der Anwendungsfall zu nischenhaft ist.

Genau das bekomme ich, wenn ich versuche, eine Methode zu verwenden, die von einem Dekorateur zu einer Klassendefinition hinzugefügt wurde. Ich muss es derzeit umgehen, indem ich entweder der Klasse eine Definition hinzufüge oder die Schnittstellenzusammenführung verwende, als any usw.

Ich habe jede Frage darüber beantwortet, was this ist und wo.

Die einfache Tatsache ist, dass sich die Bedeutung von this ändert, je nachdem, wo Sie sich befinden.

type Constructor<T> = new (...args: any[]) => T
type Greeter = { hello(): string }

function logger<T extends Constructor<Greeter>>(Class: T) {
    return class {
        hello() {
            console.log(new Class().hello())
        }
    };
}

class Foo {
    foo() { return "bar" }
    hello() { return this.foo(); } /// <------
}

const LoggableFoo = logger(Foo)
new LoggableFoo().hello() // Logs "bar"

Wenn Sie new Class() sagen, zeigt Class auf das ursprüngliche Foo, und aus der Sicht von TypeScript hätte es Zugriff auf hello(): string , da Class so eingegeben wird (erweitert Greeter). Zur Laufzeit instanziieren Sie eine Instanz des ursprünglichen Foo.

new LoggableFoo().hello() ruft zufällig eine void-Methode auf, die zufällig eine Methode aufruft, die sonst nicht erreichbar ist, eingegeben über Greeter.

Wenn Sie fertig wären

<strong i="21">@logger</strong>
class Foo {
    foo() { return "bar" }
    hello() { return this.foo(); }
}

dann ist Foo jetzt eine Klasse mit nur hello(): void und new Foo().foo() sollte ein Typfehler sein.

Und noch einmal, hello() { return this.foo(); } ist kein TypeError - warum sollte das so sein? Nur weil diese Klassendefinition nicht mehr erreichbar ist, wird diese Methode nicht irgendwie ungültig.

Ich erwarte nicht, dass TypeScript einen dieser Randfälle perfekt hinbekommt; Es wäre ziemlich verständlich, wie immer hier und da Typanmerkungen hinzufügen zu müssen. Aber keines dieser Beispiele zeigt, warum @logger nicht grundlegend ändern kann, woran Foo gebunden ist.

Wenn logger eine Funktion ist, die eine neue Klasse zurückgibt, dann verweist Foo jetzt darauf.

Ich habe jede Frage darüber beantwortet, was das ist und wo.
Die einfache Tatsache ist, dass sich die Bedeutung von this ändert, je nachdem, wo Sie sich befinden.

Das ist wirklich frustrierend. Gut, es ändert sich, es ist Foo , es ist eine statische Bindung usw. usw. usw. Was ist die Typsignatur? Welche Mitglieder hat this ? Sie sprechen über alles unter der Sonne, wenn alles, was ich von Ihnen brauche, eine einfache Typensignatur für das ist, was this in hello .

new LoggableFoo().hello() ruft zufällig eine void-Methode auf, die zufällig eine Methode aufruft, die sonst nicht erreichbar ist, eingegeben über Greeter.

Das ist nicht dasselbe wie unerreichbar. Jede erreichbare Methode ist "sonst unerreichbar", wenn man die Pfade außer Acht lässt, über die sie erreichbar ist.

Wenn du es getan hättest:

Aber das ist buchstäblich das, was ich getan habe. Das ist genau mein Code-Snippet, wieder eingefügt, vorangestellt mit einer Erklärung des Beispiels, das ich für Sie konstruiert habe. Ich habe es sogar einem Diff-Checker unterzogen, um sicherzustellen, dass ich keine verrückten Pillen genommen habe, und der einzige Unterschied ist der Kommentar, den Sie entfernt haben.

Und noch einmal, hello() { return this.foo(); } ist kein TypeError - warum sollte das so sein?

Weil Sie (und andere hier) möchten, dass der Typ von this der instanziierte Rückgabetyp des Dekorators ist, der { hello(): void } ist (beachten Sie das Fehlen eines foo -Mitglieds). Wenn Sie möchten, dass die Mitglieder von Foo this als Rückgabetyp des Dekorators sehen können, ist der Typ von this innerhalb hello { hello(): void } . Wenn es { hello(): void } ist, erhalte ich einen Typfehler. Wenn ich einen Tippfehler erhalte, bin ich traurig, weil mein Code gut läuft.

Wenn Sie sagen, dass es sich nicht um einen Typfehler handelt, geben Sie Ihr eigenes Schema auf, um den Typ von this über den Rückgabetyp des Dekorators bereitzustellen. Der Typ von this ist dann { hello(): string; bar(): string } , unabhängig davon, was der Dekorateur zurückgibt. Möglicherweise haben Sie ein alternatives Schema zur Erzeugung des Typs von this , das dieses Problem vermeidet, aber Sie müssen angeben, was es ist.

Sie scheinen nicht zu verstehen, dass Foo nach dem Ausführen der Dekorateure auf etwas verweisen kann, das sich völlig von dem unterscheidet, als was es ursprünglich definiert wurde.

function a(C){
    return class {
        x(){}
        y(){}
        z(){}
    }
}

<strong i="7">@a</strong>
class Foo {
    a(){ 
        this.b();  //valid
    }
    b() { 
        this.c();  //also valid
    }
    c(){ 
        return 0;
    }
}

let f = new Foo();
f.x(); //valid
f.y(); //also valid
f.z(); //still valid

Ich nehme an, Sie finden etwas Seltsames darin, dass this etwas anderes innerhalb von Foo oben ist, als in Instanzen, die später aus dem erstellt werden, was Foo letztendlich ist (nachdem die Dekorateure ausgeführt wurden)?

Ich bin mir nicht sicher, was ich dir sagen soll; So arbeiten Dekorateure. Mein einziges Argument ist, dass TypeScript besser mit dem übereinstimmen sollte, was passiert.

Anders ausgedrückt, die Typsignaturen im (ursprünglichen) Foo unterscheiden sich von dem, was Foo ist/produziert, sobald die Dekorateure gelaufen sind.

Um eine Analogie aus einer anderen Sprache zu leihen, innerhalb des verzierten Foo würde dies auf das Äquivalent eines anonymen C#-Typs verweisen – ein völlig echter Typ, der ansonsten gültig ist, nur nicht wirklich direkt referenziert werden kann. Es würde seltsam aussehen, die oben beschriebenen Fehler und Nicht-Fehler zu erhalten, aber noch einmal, so funktioniert es. Dekorateure geben uns enorme Kraft, um solche bizarren Dinge zu tun.

Ich vermute, Sie finden etwas Seltsames darin, dass dies etwas anderes in Foo oben ist, als in Instanzen, die später aus dem erstellt werden, was Foo schließlich ist (nachdem die Dekorateure ausgeführt wurden)?

Nein. Ich finde das nicht seltsam, weil es genau das ist, was ich vor 200 Kommentaren vorgeschlagen habe. Haben Sie die vorangegangene Diskussion überhaupt gelesen?

Das von Ihnen gepostete Snippet ist völlig unstrittig. Die Leute, mit denen ich nicht einverstanden war und denen Sie zu Hilfe gesprungen sind, wollen zusätzlich Folgendes:

function a(C){
    return class {
        x(){}
        y(){}
        z(){}
    }
}

<strong i="10">@a</strong>
class Foo {
    a(){ 
        this.b();  //valid
    }
    b() { 
        this.c();  //also valid
    }
    c(){ 
        return 0;
    }
    d(){
        // HERE: All of these are also expected to be valid
        this.x();
        this.y();
        this.z();
    }
}

let f = new Foo();
f.x(); //valid
f.y(); //also valid
f.z(); //still valid

Ich bin nicht der Meinung, dass es möglich ist, dies vernünftig zu tun, und habe verzweifelt versucht, herauszufinden, wie ein solcher Vorschlag funktionieren würde. Trotz meiner besten Bemühungen kann ich keine umfassende Liste der Mitglieder herausziehen, die this erwartet, oder wie eine solche Liste unter dem Vorschlag aufgebaut werden würde.

Anders ausgedrückt, die Typsignaturen im (ursprünglichen) Foo unterscheiden sich von dem, was Foo ist/produziert, sobald die Dekorateure gelaufen sind.

Warum streitest du überhaupt mit mir? Ich sagte: "Ich stimme zu, dass das neue Foo().foo ein Typfehler ist, und zwar ein leicht zu behebender Fehler. Ich bin nicht damit einverstanden, dass this.foo zurückgegeben wird; ein Typfehler ist ein Fehler". Analog stimme ich in Ihrem Beispiel zu, dass new Foo().x() ein Typfehler ist, ein Fehler ist, this.x() jedoch kein Typfehler ist.

Siehst du, dass es im Snippet oben auf dieser Seite zwei Kommentare gibt?

        return this.foo; // Property 'foo' does not exist on type 'Foo'

^ Das finde ich problematisch. Entweder stimmen Sie zu, dass der Rückgabetyp des Dekorators nicht auf this und nur auf new Foo() präsentiert werden sollte, in diesem Fall streiten wir beide über nichts. Oder Sie sind nicht einverstanden und möchten diese Funktion ebenfalls, in diesem Fall ist das Snippet in Ihrem vorherigen Kommentar irrelevant.

Endlich verstehe ich deinen Punkt. Es war extrem schwierig für mich, das aus Ihrem Greeter-Code herauszubekommen, aber ich verfolge es jetzt; danke, dass du so geduldig bist.

Ich würde sagen, die einzig vernünftige Lösung wäre, dass Foo ( innerhalb von Foo) die Type Union - derp meinte ich Schnittmenge - des ursprünglichen Foo unterstützt, und was auch immer der letzte Dekorateur zurückgibt. Sie müssen das ursprüngliche Foo für verrückte Beispiele wie Ihren Greeter unterstützen, und Sie müssen auf jeden Fall alles unterstützen, was der letzte Dekorateur zurückgibt, da dies der springende Punkt bei der Verwendung von Dekoratoren ist (gemäß den vielen Kommentaren oben).

Also ja, aus meinem letzten Beispiel, innerhalb von Foo x, y, z, a, b, c würde alles funktionieren. Wenn es zwei Versionen von a gäbe, dann unterstützen Sie beide.

@arackaf np, danke auch für deine Geduld. Meine Beispiele waren nicht die klarsten, weil ich einfach alles poste, was ich auf dem Spielplatz kochen kann, das zeigt, wie es kaputt ist. Es fällt mir schwer, systematisch darüber nachzudenken.

Ich würde sagen, die einzig vernünftige Lösung wäre, dass Foo (innerhalb von Foo) die Typunion des ursprünglichen Foo unterstützt, und was auch immer der letzte Dekorateur zurückgibt.

Ok, großartig, also kommen wir jetzt zu den Einzelheiten. Korrigieren Sie mich, wenn ich das falsch verstehe, aber wenn Sie "Vereinigung" sagen, meinen Sie, dass es die Typmitglieder von beiden haben sollte, dh es sollte A & B sein. Wir wollen also, dass this typeof(new OriginalClass()) & typeof(new (decorators(OriginalClass))) ist, wobei decorators die zusammengesetzte Typsignatur aller Dekorateure ist. Im Klartext wollen wir, dass this die Schnittmenge des instanziierten Typs der „Originalklasse“ und des instanziierten Typs der „Originalklasse“ ist, der durch alle Dekorateure geleitet wird.

Dabei gibt es zwei Probleme. Einer ist, dass Sie in Fällen wie meinem Beispiel nur auf nicht vorhandene Mitglieder zugreifen können. Ich könnte eine Reihe von Mitgliedern im Decorator hinzufügen, aber wenn ich versuchen würde, auf sie in meiner Klasse mit this.newMethod() zuzugreifen, würde es zur Laufzeit einfach kotzen. newMethod wird nur der von der Decorator-Funktion zurückgegebenen Klasse hinzugefügt, die Mitglieder der ursprünglichen Klasse haben keinen Zugriff darauf (es sei denn, ich verwende ausdrücklich das Muster return class extends OriginalClass { newMethod() { } } ).

Das andere Problem ist, dass "ursprüngliche Klasse" kein klar definiertes Konzept ist. Wenn ich von this aus auf dekorierte Member zugreifen kann, kann ich sie auch als Teil der return-Anweisungen verwenden, und daher können sie Teil der öffentlichen API der "ursprünglichen Klasse" sein. Ich winke hier ein bisschen mit der Hand, und ich bin ein bisschen zu ausgebrannt, um mir konkrete Beispiele auszudenken, aber ich denke, wenn wir gründlich darüber nachdenken, könnten wir uns unsinnige Beispiele einfallen lassen. Vielleicht könnten Sie dies umgehen, indem Sie einen Weg finden, Mitglieder zu trennen, die keine Dinge zurückgeben, auf die sie von this aus zugegriffen haben, oder für die zumindest der Rückgabetyp nicht als Ergebnis der Rückgabe von this.something() abgeleitet wird.

@masaeedu Ja, ich habe mich vor Ihrer Antwort in Bezug auf die Union / Schnittmenge korrigiert. Es ist kontraintuitiv für jemanden, der neu bei TS ist.

Im übrigen verstanden. Ehrlich gesagt geben Dekorateure normalerweise keinen völlig anderen Typ zurück, sie erweitern normalerweise nur den Typ, der übergeben wurde, sodass die Schnittmenge die meiste Zeit sicher "einfach funktioniert".

Ich würde sagen, die Laufzeitfehler, von denen Sie sprechen, wären selten und das Ergebnis einiger absichtlich schlechter Entwicklungsentscheidungen. Ich bin mir nicht sicher, ob Sie sich wirklich darum kümmern müssen, aber wenn es wirklich ein Problem ist, würde ich sagen, dass es ein anständiger zweiter Platz wäre, wenn Sie einfach das verwenden, was der letzte Dekorateur zurückgegeben hat (also ja, eine Klasse könnte einen TypeError von sehen versuchen, eine Methode zu verwenden, die sich selbst definiert - nicht ideal, aber immer noch ein lohnender Preis für die Arbeit von Dekorateuren).

Aber wirklich, ich denke, die Laufzeitfehler, an die Sie denken, sind es nicht wert, verhindert zu werden, sicherlich auf Kosten der korrekt arbeitenden Dekorateure. Außerdem ist es ziemlich einfach, Laufzeitfehler in TS zu produzieren, wenn Sie nachlässig oder dumm sind.

interface C { a(); }
class C {
    foo() {
        this.a();  //<--- boom
    }
}

let c = new C();
c.foo();

Zu deinem zweiten Einwand

Ich kann sie auch als Teil der return-Anweisungen verwenden, und daher können sie Teil der öffentlichen API der "ursprünglichen Klasse" sein

Ich fürchte, da sehe ich keine Probleme. Ich möchte , dass alles, was der Klasse über einen Dekorateur hinzugefügt wird, absolut ein erstklassiger Bürger ist. Ich bin gespannt, welche möglichen Probleme es geben könnte.

Ich denke, eine andere gute Option wäre, dies nur teilweise zu implementieren.

Derzeit werden Klassen immer so typisiert, wie sie definiert sind. Innerhalb von Foo basiert dies also darauf, wie foo definiert ist, unabhängig von Dekorateuren. Es wäre eine riesige, riesige Verbesserung, dies "nur" für einige nützliche Untergruppen von Decorator-Anwendungsfällen (dh die häufigsten) zu erweitern.

Was ist, wenn Sie zulassen, dass die Klasse erweitert wird (aus der Perspektive von this innerhalb der Klasse), wenn und nur wenn der Dekorateur etwas zurückgibt, das das Original erweitert, dh for

function d(Class) {
    return class extends Class {
        blah() { }
    };
}

<strong i="9">@d</strong>
class Foo {
    a() { }
    b() { }
    c() { 
        this.blah(); // <---- valid
    }
}

Lassen Sie es einfach funktionieren und bieten Sie erstklassige Unterstützung für blah und alles andere, was der Dekorateur hinzufügt. Und für Anwendungsfälle, die verrückte Dinge tun, wie die Rückgabe einer ganz neuen Klasse (wie Ihr Greeter), setzen Sie einfach das aktuelle Verhalten fort und ignorieren Sie, was der Dekorateur tut.


Übrigens, unabhängig davon, was Sie wählen, wie würde ich das kommentieren? Kann dies aktuell kommentiert werden? Ich habe es versucht

function d<T>(Class: new() => T): T & { new (): { blah(): void } } {
    return class extends Class {
        blah() { }
    };
}

sowie viele Variationen zu diesem Thema, aber TypeScript hatte nichts davon :)

@arackaf Ein Dekorateur ist nur eine Funktion. Im Allgemeinen erhalten Sie es irgendwo aus einer .d.ts -Datei und haben keine Ahnung, wie es implementiert ist. Sie wissen nicht, ob die Implementierung eine ganz neue Klasse zurückgibt, Mitglieder des Prototyps der ursprünglichen Klasse hinzufügt/subtrahiert/ersetzt und zurückgibt oder die ursprüngliche Klasse erweitert. Alles, was Ihnen zur Verfügung steht, ist der strukturelle Rückgabetyp der Funktion.

Wenn Sie Decorators irgendwie an die Klassenvererbung binden wollen, müssen Sie zuerst ein separates Sprachkonzept für JS vorschlagen. Die Art und Weise, wie Dekorateure heute arbeiten, rechtfertigt es nicht, this im Allgemeinen zu mutieren. Als Beispiel bevorzuge ich persönlich immer die Komposition gegenüber der Vererbung und würde immer Folgendes tun:

function logger<T extends Constructor<Greeter>>(Class: T) {
    return class {
        readonly _impl;
        constructor() {
            this._impl = new Class()
        }
        // Use _impl ...
    };
}

Dies ist kein verrücktes akademisches Experiment, es ist ein dämlicher Standardansatz für Mixins und bricht mit dem Beispiel, das ich Ihnen gegeben habe. Tatsächlich bricht fast alles außer return class extends Class mit dem Beispiel, das ich Ihnen gegeben habe, und in vielen Fällen werden die Dinge mit return class extends Class .

Sie müssen durch alle möglichen Hürden und Verrenkungen springen, damit dies funktioniert, und das Typensystem wird Sie bei jedem Schritt des Weges bekämpfen, weil das, was Sie tun, im Allgemeinen unsinnig ist. Noch wichtiger ist, dass jeder, sobald Sie es implementiert haben, akrobatisch um diese unsinnige Ecke des Typsystems manövrieren muss, wenn er versucht, ein anderes komplexes (aber solides) Konzept zu implementieren.

Nur weil Sie diesen einen Anwendungsfall haben, den Sie für wichtig halten (und ich habe in diesem Thread mehrmals versucht, alternative Wege aufzuzeigen, um das, was Sie wollen, im bestehenden Typsystem vernünftig auszudrücken), heißt das nicht, dass dies das einzig Richtige ist zu tun ist, mit Ihrem vorgeschlagenen Ansatz um jeden Preis weiterzupfuschen. Sie können beliebige Schnittstellen mit Ihrer Klasse zusammenführen, einschließlich der Rückgabetypen von Decorator-Funktionen. Es ist also nicht so, dass es für Sie unmöglich wäre, irgendwohin zu gelangen, wenn Sie darauf bestehen, Decorators so zu verwenden, wie Sie sie verwenden.

@arackaf das:

function d<T>(Class: new() => T): T & { new (): { blah(): void } } {
    return class extends Class {
        blah() { }
    };
}

Sollte in der Extends-Klausel funktionieren:

class C extends d(S) {
  foo() {
    this.blah(); // tsc is happy here
  }
}

Mit viel einfacherer und bereits definierter Semantik im Typsystem. Das funktioniert schon. Welches Problem versuchen Sie, durch Unterklassen der deklarierten Klasse zu lösen, anstatt eine Oberklasse dafür zu erstellen?

@masaeedu

Die heutige Arbeitsweise von Dekorateuren rechtfertigt es nicht, dies im Allgemeinen zu ändern.

Die primäre Verwendung für (Klassen-) Dekorateure besteht darin, den Wert von this innerhalb der Klasse auf die eine oder andere Weise positiv zu verändern. @connect Redux und $# @observer $#$ von MobX nehmen beide eine Klasse auf und spucken eine NEUE Version der ursprünglichen Klasse mit zusätzlichen Funktionen aus. In diesen speziellen Fällen glaube ich nicht, dass sich die tatsächliche Struktur von this ändert (nur die Struktur von this.props ), aber das ist unerheblich.

Es ist ein ziemlich häufiger Anwendungsfall (wie Sie den obigen Kommentaren entnehmen können), einen Dekorierer zu verwenden, um eine Klasse irgendwie zu mutieren). Menschen neigen wohl oder übel dazu, die syntaktischen Alternativen einfach nicht zu mögen

let Foo = a(b(c(
    class Foo {
    }
})));

im Gegensatz zu

<strong i="19">@a</strong>
<strong i="20">@b</strong>
<strong i="21">@c</strong>
class Foo {
}

Nun, ob ein Dekorateur tut

function d (Class){
    Class.prototype.blah = function(){
    };
}

oder

function d(Class){
    return class extends Class {
        blah(){ }
    }
}

sollte keine Rolle spielen. Auf die eine oder andere Weise besteht der Anwendungsfall, nach dem viele wirklich schreien, darin, TypeScript durch alle erforderlichen Anmerkungen mitteilen zu können, egal wie unbequem für uns, dass function c eine Klasse C ein und gibt eine Klasse zurück, die die Struktur C & {blah(): void} hat.

Dafür verwenden viele von uns heute aktiv Dekorateure. Wir wollen einfach wirklich, wirklich eine nützliche Teilmenge dieses Verhaltens in das Typsystem integrieren.

Es ist leicht zu demonstrieren, dass Dekorateure bizarre Dinge tun können, die für das Typsystem unmöglich zu verfolgen wären. Bußgeld! Aber es muss eine Möglichkeit geben, das zu kommentieren

<strong i="38">@c</strong>
class Foo {
    hi(){ this.addedByC(); }
}

ist gültig. Ich weiß nicht, ob für c eine neue Typannotationssyntax erforderlich ist oder ob die vorhandene Typannotationssyntax für c in Betrieb genommen werden könnte, um dies zu erreichen, aber ich behebe nur die allgemeine Verwendung Fall (und die Kanten so zu belassen, wie sie sind, ohne Auswirkungen auf die ursprüngliche Klasse) wäre ein großer Segen für TS-Benutzer.

@justinfagnani zur Info

function d<T>(Class: new() => T): T & { new (): { blah(): void } } {
    return class extends Class {
        blah() { }
    };
}

produziert

Der Typ „typeof (anonyme Klasse)“ kann dem Typ „T & (new () => { blah(): void; })“ nicht zugewiesen werden.
Der Typ „typeof (anonyme Klasse)“ kann dem Typ „T“ nicht zugewiesen werden.

auf dem TS-Spielplatz. Ich bin mir nicht sicher, ob ich einige Optionen falsch eingestellt habe.

Welches Problem versuchen Sie, durch Unterklassen der deklarierten Klasse zu lösen, anstatt eine Oberklasse dafür zu erstellen?

Ich versuche lediglich, Dekorateure zu verwenden. Ich benutze sie seit über einem Jahr glücklich in JS - ich bin nur gespannt darauf, dass TS aufholt. Und ich bin sicherlich nicht mit Unterklassen verheiratet. Manchmal mutiert der Dekorateur einfach den Prototyp der ursprünglichen Klasse, um Eigenschaften oder Methoden hinzuzufügen. In jedem Fall besteht das Ziel darin, TypeScript dies mitzuteilen

<strong i="17">@c</strong>
class Foo { 
}

führt zu einer Klasse mit neuen Mitgliedern, erstellt von c .

@arackaf Wir geraten wieder aus dem Takt. Die Debatte dreht sich nicht darum:

<strong i="8">@a</strong>
<strong i="9">@b</strong>
<strong i="10">@c</strong>
class Foo {
}

dagegen:

let Foo = a(b(c(
    class Foo {
    }
})));

Ich gehe davon aus, dass Sie in der Lage sind, Ersteres zu verwenden und trotzdem new Foo() korrekt einzugeben. Die Debatte dreht sich darum:

<strong i="18">@a</strong>
<strong i="19">@b</strong>
<strong i="20">@c</strong>
class Foo {
    fooMember() { 
        this.aMember(); this.bMember(); this.cMember(); 
    }
}

dagegen:

class Foo extends (<strong i="24">@a</strong> <strong i="25">@b</strong> <strong i="26">@c</strong> class { }) {
    fooMember() { 
        this.aMember(); this.bMember(); this.cMember(); 
    }
}

Letzteres funktioniert, ohne das Typsystem zu brechen. Ersteres ist in mehrfacher Hinsicht problematisch, die ich bereits erläutert habe.

@masaeedu gibt es wirklich keine eingeschränkten Anwendungsfälle, in denen erstere zum Laufen gebracht werden könnten?

Um genau zu sein: Gibt es keine Anmerkungen, die TypeScript uns mitteilen könnte, a , b und c aus Ihrem obigen Beispiel hinzuzufügen, damit das Typsystem die ersteren behandelt ununterscheidbar von letzterem?

Das wäre ein Killer-Fortschritt für TypeScript.

@arackaf Entschuldigung, Sie müssen den Typparameter auf den Konstruktortyp verweisen lassen, nicht auf den Instanztyp:

Das funktioniert:

type Constructor<T = object> = new (...args: any[]) => T;

interface Blah {
  blah(): void;
}

function d<T extends Constructor>(Class: T): T & Constructor<Blah> {
  return class extends Class {
    blah() { }
  };
}

class Foo extends d(Object) {
  protected num: number;

  constructor(num: number) {
    super();
    this.num = num;
    this.blah();
  }
}

Ich versuche lediglich, Dekorateure zu verwenden.

Das ist nicht wirklich ein Problem, das Sie zu lösen versuchen, und es ist eine Tautologie. Es ist wie F: "Was versuchen Sie mit diesem Hammer zu tun?" A: "Benutze einfach den Hammer".

Gibt es einen konkreten Fall, an den Sie denken, wo das Generieren einer neuen Oberklasse mit der beabsichtigten Erweiterung nicht funktioniert? Da TypeScript dies jetzt unterstützt, und wie ich bereits sagte, ist es in diesem Fall viel einfacher, über den Typ der Klasse innerhalb der Klasse nachzudenken.

Ich habe mir die Annotation @observer Mobx angesehen und es sieht so aus, als würde sie die Klassenform nicht ändern (und könnte leicht eher ein Methoden-Decorator als ein Klassen-Decorator sein, da sie nur render() umschließt).

@connect ist eigentlich ein großartiges Beispiel für die Komplexität der korrekten Eingabe von Dekoratoren, da @connect anscheinend nicht einmal eine Unterklasse der dekorierten Klasse zurückgibt, sondern eine völlig neue Klasse, die die dekorierte Klasse noch umschließt Die Statik wird kopiert, sodass das Ergebnis die instanzseitige Schnittstelle wegwirft und die statische Seite beibehält, die TS in den meisten Fällen nicht einmal überprüft. Es sieht so aus, als ob @connect wirklich überhaupt kein Dekorateur sein sollte und connect nur als HOC verwendet werden sollte, weil es als Dekorateur furchtbar verwirrend ist.

Ich benutze sie glücklich seit weit über einem Jahr in JS

Nun... hast du nicht wirklich. JS hat keine Dekorateure. Sie haben wahrscheinlich einen bestimmten nicht mehr existierenden früheren Vorschlag verwendet, der in Babel und TypeScript etwas anders implementiert ist. An anderen Orten plädiere ich dafür, dass Dekorateure den Standardisierungsprozess fortsetzen, aber der Fortschritt hat sich verlangsamt, und ich glaube nicht, dass dies noch eine Gewissheit ist. Viele argumentieren, dass sie nicht hinzugefügt werden sollten oder nur Anmerkungen sein sollten, sodass die Semantik möglicherweise noch in der Luft liegt.

Eine der Beschwerden gegen Dekorateure ist genau, dass sie die Form der Klasse ändern können, was es schwierig macht, darüber statisch zu argumentieren, und es wurde über einige Vorschläge gesprochen, um die Fähigkeit der Dekorateure, dies zu tun, zumindest einzuschränken, obwohl ich denke, dass das vor dem war Der jüngste Vorschlag hat eine Spezifikationssprache. Ich persönlich behaupte, dass gut erzogene Dekorateure die Form der Klasse nicht ändern sollten, damit sie für die Zwecke der statischen Analyse ignoriert werden können. Denken Sie daran, dass Standard-JS kein Typsystem hat, auf das Sie sich stützen können, um der Analyse mitzuteilen, was eine Funktionsimplementierung tut.

  • Ich bin nur begierig darauf, dass TS aufholt.

Ich sehe nicht, wie TypeScript hier in irgendeiner Weise zurückliegt. Zumindest hinter was? Dekorateure arbeiten. Gibt es ein anderes typisiertes JS, in dem sich Klassen so verhalten, wie Sie es möchten?

Decorators sind jetzt Phase 2 und werden in fast jedem JS-Framework verwendet.

Was @connect betrifft, so liegt React-Redux bei etwa 2,5 Millionen Downloads pro Monat; es ist unglaublich arrogant zu hören, dass das Design irgendwie "falsch" ist, weil man es anders umgesetzt hätte.

Gegenwärtige Versuche, Methoden zu verwenden, die von einem Dekorateur eingeführt wurden, führen zu Typfehlern in TypeScript, was Umgehungslösungen wie das Zusammenführen von Schnittstellen, das manuelle Hinzufügen der Anmerkung für die hinzugefügten Methoden oder einfach das Nichtverwenden von Dekoratoren zum Mutieren von Klassen erfordert (als ob jemandem gesagt werden müsste, dass wir es tun könnten Verwenden Sie keine Dekorateure).

Gibt es wirklich keine Möglichkeit, den TS-Compiler manuell und gewissenhaft darüber zu informieren, dass ein Dekorateur die Form einer Klasse ändert? Vielleicht ist das verrückt, aber TS konnte nicht einfach einen neuen Dekorateur liefernTyp, den wir verwenden könnten

sei c : Dekorateur

Und WENN (und nur wenn) Dekoratoren dieses Typs auf eine Klasse angewendet werden, betrachtet TS die Klasse als vom Typ Input & { blah() } ?

Buchstäblich jeder weiß, dass wir Dekorateure einfach nicht verwenden können. Die meisten wissen auch, dass eine Gesangsgruppe Dekorateure nicht mag. In diesem Beitrag geht es darum, wie TS möglicherweise irgendwie sein Typsystem dazu bringen könnte, zu verstehen, dass ein Dekorateur eine Klassendefinition mutiert. Das wäre für viele Menschen ein großer Segen.

Um genau zu sein: Gibt es keine Anmerkungen, die TypeScript uns mitteilen könnte, a, b und c aus Ihrem obigen Beispiel hinzuzufügen, damit das Typsystem Ersteres ununterscheidbar von Letzterem behandelt?

Ja, es ist sehr einfach, jede gewünschte Form für Foo zu deklarieren, Sie müssen nur die Schnittstellenzusammenführung verwenden. Sie müssen nur Folgendes tun:

interface Foo extends <whateverextramembersiwantinfoo> { } 
<strong i="9">@a</strong>
<strong i="10">@b</strong>
<strong i="11">@c</strong>
class Foo { 
    /* have fun */
}

Im Moment müssen Sie wahrscheinlich davon ausgehen, dass die von Ihnen verwendete Decorator-Bibliothek parametrisierte Typen exportiert, die dem entsprechen, was sie zurückgeben, oder Sie müssen die Member deklarieren, die sie selbst hinzufügen. Wenn wir #6606 erhalten, können Sie einfach interface Foo extends typeof(a(b(c(Foo)))) tun.

Gegenwärtige Versuche, Methoden zu verwenden, die von einem Dekorateur eingeführt wurden, führen zu Typfehlern in TypeScript, was Umgehungslösungen wie das Zusammenführen von Schnittstellen, das manuelle Hinzufügen der Anmerkung für die hinzugefügten Methoden oder einfach das Nichtverwenden von Dekoratoren zum Mutieren von Klassen erfordert (als ob jemandem gesagt werden müsste, dass wir es tun könnten Verwenden Sie keine Dekorateure).

Sie haben nicht die Option erwähnt, die Klasse, die Sie schreiben, zu dekorieren, wenn sie für die vom Dekorateur eingeführten Mitglieder agnostisch ist, und sie zu einer dekorierten Klasse zu erweitern, wenn dies nicht der Fall ist. So werde ich zumindest Dekorateure verwenden.

Könnten Sie mir ein Code-Snippet aus einer Bibliothek geben, die Sie gerade verwenden, wo dies ein Schmerzpunkt ist?

Entschuldigung - ich bin Ihrem letzten Kommentar nicht gefolgt, aber Ihr Kommentar davor, mit der Zusammenführung der Benutzeroberfläche, ist genau so, wie ich es umgehe.

Derzeit muss mein Dekorateur auch einen Typ exportieren, und ich verwende die Schnittstellenzusammenführung genau so, wie Sie es gezeigt haben, um TS anzuzeigen, dass neue Mitglieder vom Dekorateur hinzugefügt werden. Es ist die Fähigkeit, diese zusätzliche Boilerplate loszuwerden, auf die viele so gespannt sind.

@arackaf Vielleicht möchten Sie wirklich, dass das Zusammenführen von Deklarationen mit der kontextbezogenen Typisierung von Funktionsargumenten interagiert.

Sicher. Ich möchte die Möglichkeit haben, einen Dekorator für eine Klasse zu verwenden, und konzeptionell, basierend darauf, wie ich den Dekorator definiert habe, dass eine Schnittstellenzusammenführung stattfindet, was dazu führt, dass meiner dekorierten Klasse zusätzliche Mitglieder hinzugefügt werden.

Ich habe keine Ahnung, wie ich meinen Decorator kommentieren würde, um das zu erreichen, aber ich bin nicht wirklich wählerisch - jede Syntax, die mir eine zukünftige TS-Version geben würde, würde mich unglaublich glücklich machen :)

Decorators sind jetzt Phase 2 und werden in fast jedem JS-Framework verwendet.

Und sie sind erst in Stufe 2 und bewegen sich extrem langsam. Ich möchte unbedingt, dass Dekorateure entstehen, und ich versuche herauszufinden, wie ich ihnen helfen kann, voranzukommen, aber sie sind immer noch keine Gewissheit. Ich kenne immer noch Leute, die Einfluss auf den Prozess haben könnten, die dagegen protestieren oder wollen, dass sie sich auf Anmerkungen beschränken, obwohl ich nicht _glaube_, dass sie sich einmischen werden. Mein Punkt, dass aktuelle Compiler-Decorator-Implementierungen kein JS sind, steht.

Was @connect betrifft , so liegt React-Redux bei etwa 2,5 Millionen Downloads pro Monat; es ist unglaublich arrogant zu hören, dass das Design irgendwie "falsch" ist, weil man es anders umgesetzt hätte.

Bitte unterlassen Sie persönliche Angriffe wie mich als arrogant zu bezeichnen.

  1. Ich habe Sie nicht persönlich angegriffen, also greifen Sie mich nicht persönlich an.
  2. Es hilft Ihrer Argumentation überhaupt nicht.
  3. Die Popularität eines Projekts sollte es nicht von der technischen Kritik ausschließen.
  4. Reden wir vom selben? redux-connect-decorator hat 4 Downloads pro Tag. Die Funktion connect() respond-redux sieht aus wie ein HOC, wie ich vorgeschlagen habe.
  5. Ich denke, dass meine Meinung, dass gut erzogene Dekorateure die öffentliche Form einer Klasse nicht ändern und sie auf jeden Fall durch eine nicht verwandte Klasse ersetzen sollten, ziemlich vernünftig ist und in der JS-Community genügend Zustimmung finden würde. Es ist ziemlich weit von Arroganz entfernt, auch wenn Sie anderer Meinung sind.

Gegenwärtige Versuche, Methoden zu verwenden, die von einem Dekorateur eingeführt wurden, führen zu Typfehlern in TypeScript, was Umgehungslösungen wie das Zusammenführen von Schnittstellen, das manuelle Hinzufügen der Anmerkung für die hinzugefügten Methoden oder einfach das Nichtverwenden von Dekoratoren zum Mutieren von Klassen erfordert (als ob jemandem gesagt werden müsste, dass wir es tun könnten Verwenden Sie keine Dekorateure).

Es ist nicht so, dass wir nur vorschlagen, Dekoratoren überhaupt nicht zu verwenden, sondern ich glaube, dass @masaeedu sagt, dass wir zum Zwecke der typsicheren Änderung eines Prototyps über bestehende Mechanismen verfügen: Vererbung und Mixin-Anwendung. Ich habe versucht zu fragen, warum Ihr d -Beispiel nicht für die Verwendung als Mixin geeignet ist, und Sie haben keine Antwort gegeben, außer zu sagen, dass Sie nur Dekorateure verwenden wollten.

Das ist in Ordnung, denke ich, aber scheint das Gespräch ziemlich kritisch einzuschränken, also verbeuge ich mich wieder.

@justinfagnani , von dem ich gesprochen habe

import {connect} from 'react-redux'

connect Es gibt nur eine Funktion, die eine Klasse (Komponente) aufnimmt und eine andere ausspuckt, wie Sie oben sagten.

Derzeit habe ich sowohl in Babel als auch in TypeScript die Möglichkeit, connect wie folgt zu verwenden

class UnConnectedComponent extends Component {
}

let Component = connect(state => state.foo)(UnConnectedComponent);

ODER so

@connect(state => state.foo)
class Component extends Component {
}

Ich bevorzuge zufällig Letzteres, aber wenn Sie oder jemand anderes Ersteres bevorzugen, dann sei es so; Viele Wege führen nach Rom. Ich bevorzuge ähnlich

<strong i="19">@mappable</strong>
<strong i="20">@vallidated</strong>
class Foo {
}

über

let Foo = validated(mappable(class Foo {
}));

oder

class Foo extends mixin(validated(mappable)) {
}
//or however that would look.

Ich habe Ihre Frage nicht explizit beantwortet, nur weil ich dachte, der Grund, warum ich Dekorateure verwende, ist klar, aber ich sage es ausdrücklich: Ich bevorzuge die Ergonomie und Syntax, die Dekorateure bieten. In diesem ganzen, ganzen Thread geht es darum, TS irgendwie mitteilen zu können, wie unsere Dekorateure die betreffende Klasse mutieren, damit wir keine Boilerplate-ähnliche Schnittstellenzusammenführung benötigen.

Wie OP vor langer Zeit sagte, wir wollen nur

declare function Blah<T>(target: T): T & {foo: number}

<strong i="31">@Blah</strong>
class Foo {
    bar() {
        return this.foo; // Property 'foo' does not exist on type 'Foo'
    }
}

new Foo().foo; // Property 'foo' does not exist on type 'Foo'

nur zu arbeiten. Die Tatsache, dass andere Alternativen zu dieser spezifischen Syntax existieren, ist sowohl offensichtlich als auch unerheblich. Die Tatsache, dass viele in der JS/TS-Community diese spezielle Syntax nicht mögen, ist ebenfalls offensichtlich und unerheblich.

Wenn TS uns irgendeine Möglichkeit geben könnte, diese Syntax zum Laufen zu bringen, wäre dies ein großer Vorteil für die Sprache und die Gemeinschaft.

@arackaf Du hast es versäumt zu erwähnen, dass die Verwendung connect den Zugriff auf zusätzliche Mitglieder auf this $ erfordert. AFAICT, Ihre Komponentenimplementierung soll völlig unabhängig von dem sein, was connect tut, sie verwendet nur ihre eigenen props und state . In diesem Sinne stimmt die Art und Weise, wie connect gestaltet ist, eher mit der Verwendung von Dekorateuren durch @justinfagnani überein als mit Ihrer.

Nicht wirklich. Erwägen

@connect(state => state.stuffThatSatisfiesPropsShape)
class Foo extends Component<PropsShape, any> {
}

dann später

<Foo />

das sollte gültig sein - PropsShape-Requisiten stammen aus dem Redux-Store, aber TypeScript weiß das nicht, da es von der ursprünglichen Definition von Foo abweicht, sodass ich am Ende Fehler für fehlende Requisiten erhalte und es als any muss

Aber um es klar zu sagen, der genaue Anwendungsfall, der ursprünglich von OP angegeben und in meinem zweiten Kommentar hier oben dupliziert wurde, ist in unserer Codebasis äußerst verbreitet, und so wie es aussieht, auch bei vielen anderen. Wir verwenden buchstäblich Dinge wie

<strong i="6">@validated</strong>
<strong i="7">@mappable</strong>
class Foo {
}

und müssen einige Schnittstellenzusammenführungen hinzufügen, um TypeScript zufrieden zu stellen. Es wäre wunderbar, eine Möglichkeit zu haben, diese Schnittstelle transparent in die Decorator-Definition einzufügen.

Damit drehen wir uns weiter im Kreis. Wir sind uns einig , dass der Typ von Foo der Rückgabetyp von connect sein sollte. Darin sind wir uns einig.

Wo wir uns unterscheiden, ist, ob Mitglieder innerhalb Foo vorgeben müssen, dass this eine Art rekursive Verschmelzung des Rückgabetyps des Dekorators und des "ursprünglichen Foo" ist, was auch immer das bedeutet. Sie haben dafür keinen Anwendungsfall aufgezeigt.

Wo wir uns unterscheiden, ist, ob Mitglieder innerhalb von Foo so tun müssen, als ob dies eine Art rekursive Verschmelzung des Dekorator-Rückgabetyps und des "ursprünglichen Foo" sei, was auch immer das bedeutet. Sie haben dafür keinen Anwendungsfall aufgezeigt.

Ich habe. Siehe meinen Kommentar oben und den Originalcode von OP. Dies ist ein sehr häufiger Anwendungsfall.

Bitte zeigen Sie mir, was connect dem Prototyp hinzufügt, auf den Sie innerhalb der Komponente zugreifen sollen. Wenn die Antwort "nichts" lautet, sprechen Sie connect bitte nicht noch einmal an, da dies völlig irrelevant ist.

@masaeedu fair genug, und ich stimme zu. Ich habe hauptsächlich auf die Behauptungen von @justinfagnani geantwortet , dass Dekorateure die ursprüngliche Klasse nicht ändern sollten, connect wurde schlecht entworfen usw. usw.

Ich habe lediglich die Syntax demonstriert, die ich und viele andere bevorzugen, ungeachtet der Tatsache, dass andere Optionen existieren.

Ja, für diesen Fall kenne ich keine beliebten npm-Bibliotheken, die eine Klasse aufnehmen und eine neue Klasse mit neuen Methoden ausspucken, die innerhalb der dekorierten Klasse verwendet würden. Dies ist etwas, was ich und andere häufig in unserem eigenen Code tun, um unsere eigenen Mixins zu implementieren, aber ich habe nicht wirklich ein Beispiel für npm.

Aber es ist nicht wirklich umstritten, dass dies ein häufiger Anwendungsfall ist, oder?

@arackaf Nein, das tut es nicht, weil Sie auf this auf keine Mitglieder zugreifen, die von @connect eingeführt werden. Tatsächlich bin ich mir ziemlich sicher, dass dieser genaue Ausschnitt:

@connect(state => state.stuffThatSatisfiesPropsShape)
class Foo extends Component<PropsShape, any> {
    render(){
        this.props.stuffFromPropsShape // <----- added by decorator
    }
}

wird heute ohne Probleme in TypeScript kompiliert. Das von Ihnen eingefügte Snippet hat nichts mit der angeforderten Funktion zu tun.

@masaeedu - tut mir leid - ich habe gemerkt, dass ich falsch lag, und diesen Kommentar gelöscht. Nicht schnell genug, um zu verhindern, dass Sie Ihre Zeit verschwenden :(

@arackaf Ich weiß nicht, wie häufig ein Muster ist, und ich persönlich habe es weder selbst noch Bibliotheken verwendet, die es verwenden. Ich habe Angular 2 für eine Weile verwendet, also ist es nicht so, als hätte ich noch nie in meinem Leben Dekorateure verwendet. Aus diesem Grund habe ich nach spezifischen Beispielen für die Verwendung einer Bibliothek gefragt, bei der die Unfähigkeit, auf vom Dekorateur eingeführte Mitglieder im Dekorateur zuzugreifen, ein Problem darstellt.

In jedem Fall, in dem die Mitglieder einer Klasse etwas von this erwarten, sollte etwas im Körper der Klasse oder in der Klausel extends der Klasse vorhanden sein, das die Anforderung erfüllt. So hat es immer funktioniert, auch bei Dekorateuren . Wenn Sie einen Dekorator haben, der einige Funktionen hinzufügt, von denen eine Klasse abhängt, sollte die Klasse alles erweitern, was der Dekorator zurückgibt, und nicht umgekehrt. Es ist nur gesunder Menschenverstand.

Ich bin mir nicht sicher, warum das gesunder Menschenverstand ist. Dies ist derzeit Code aus unserer Codebasis. Wir haben einen Decorator, der dem Prototyp einer Klasse eine Methode hinzufügt. TypeScript-Fehler aus. Damals habe ich diese Erklärung einfach dort hingeworfen. Ich hätte Schnittstellenzusammenführung verwenden können.

Aber es wäre schön, überhaupt nichts zu verwenden. Es wäre wirklich, wirklich nett, TypeScript sagen zu können, dass „dieser Dekorateur hier diese Methode zu jeder Klasse hinzufügt, auf die sie angewendet wird – erlaube this innerhalb der Klasse auch darauf zuzugreifen“

Viele andere in diesem Thread scheinen Dekorateure ähnlich zu verwenden, also hoffe ich, dass Sie bedenken, dass es vielleicht nicht dem gesunden Menschenverstand entspricht, dass dies nicht funktionieren sollte.

image

@arackaf Ja, also solltest du export class SearchVm extends (@mappable({...}) class extends SearchVmBase {}) machen. Es ist SearchVm , das von der Mapping-Fähigkeit abhängt, nicht umgekehrt.

Exportklasse SearchVm erweitert (@mappable({...}) Klasse erweitert SearchVmBase {})

Der springende Punkt in diesem Thread ist, solche Boilerplates zu VERMEIDEN. Dekorateure bieten einen viel, viel schöneren DX, als solche Dinge tun zu müssen. Es gibt einen Grund, warum wir uns dafür entschieden haben, Code wie ich zu schreiben, und nicht so.

Wenn Sie die Kommentare zu diesem Thread lesen, werden Sie hoffentlich davon überzeugt sein, dass viele andere Leute versuchen, die einfachere Decorator-Syntax zu verwenden, und daher überdenken, was wir tun "sollten" oder was "gesunder Menschenverstand" ist usw.

Was Sie wollen, hat nichts speziell mit Dekorateuren zu tun. Ihr Problem ist, dass der Mechanismus von TypeScript, um zu sagen: „Hey TypeScript, Dinge passieren mit dieser Struktur, von denen Sie nichts wissen, lassen Sie mich Ihnen alles darüber erzählen“, derzeit zu begrenzt ist. Im Moment haben wir nur das Zusammenführen von Schnittstellen, aber Sie möchten, dass Funktionen das Zusammenführen von Schnittstellen auf ihre Argumente anwenden können, was in keiner Weise speziell mit Decorators zusammenhängt.

Im Moment haben wir nur das Zusammenführen von Schnittstellen, aber Sie möchten, dass Funktionen das Zusammenführen von Schnittstellen auf ihre Argumente anwenden können, was in keiner Weise speziell mit Decorators zusammenhängt.

In Ordnung. Meinetwegen. Möchten Sie, dass ich ein separates Thema eröffne, oder möchten Sie dieses umbenennen usw.?

@arackaf Der einzige "Boilerplate" ist die Tatsache, dass ich class { } hatte, was a) feste 7 Zeichen sind, egal wie viele Dekorateure Sie verwenden b) ein Ergebnis der Tatsache, dass Dekorateure (noch) nicht können ) auf beliebige Klassenrückgabeausdrücke angewendet werden, und c) weil ich speziell Dekoratoren zu Ihrem Vorteil verwenden wollte. Diese alternative Formulierung:

export class SearchVm extends mappable({...})(SearchVmBase)
{
}

ist nicht ausführlicher als das, was Sie ursprünglich tun.

In Ordnung. Meinetwegen. Möchten Sie, dass ich ein separates Thema eröffne, oder möchten Sie dieses umbenennen usw.?

Separate Ausgabe wäre gut.

ist nicht ausführlicher als das, was Sie ursprünglich tun

Es ist nicht mehr ausführlich, aber ich hoffe wirklich, dass Sie die Tatsache berücksichtigen, dass viele diese Syntax einfach nicht mögen. Ob gut oder schlecht, viele bevorzugen das Angebot von DX-Dekorateuren.

Separate Ausgabe wäre gut.

Ich werde morgen einen schreiben und für den Kontext auf diesen verlinken.

Danke für die Hilfe beim Durcharbeiten :)

Wenn ich eingreifen darf, @masaeedu , ich stimme dieser Aussage nicht zu:

Was Sie wollen, hat nichts speziell mit Dekorateuren zu tun. Ihr Problem ist, dass der Mechanismus von TypeScript, um zu sagen: „Hey TypeScript, Dinge passieren mit dieser Struktur, von denen Sie nichts wissen, lassen Sie mich Ihnen alles darüber erzählen“, derzeit zu begrenzt ist. Im Moment haben wir nur das Zusammenführen von Schnittstellen, aber Sie möchten, dass Funktionen das Zusammenführen von Schnittstellen auf ihre Argumente anwenden können, was in keiner Weise speziell mit Decorators zusammenhängt.

Sicherlich könnte TypeScript den Rückgabetyp des Klassen-Decorators untersuchen, um herauszufinden, welchen Typ die mutierte Klasse haben sollte. Es ist nicht erforderlich, "Schnittstellenverschmelzung auf Argumente anzuwenden". So wie ich es sehe, hat es ausschließlich mit Dekorateuren zu tun.

Auch der Fall, dass TypeScript nicht weiß, dass eine verbundene Komponente keine Requisiten mehr benötigt, ist ein Aspekt, der mich davon abgehalten hat, Redux mit React und TypeScript zu verwenden.

@codeandcats Ich habe mich damit im Kreis bewegt, seit der Thread begonnen hat, und ich kann es wirklich nicht mehr tun. Bitte sehen Sie sich die vorangegangene Diskussion genau an und versuchen Sie tatsächlich zu verstehen, worüber ich mit @arackaf nicht einverstanden war.

Ich stimme zu, dass, wenn Sie <strong i="8">@bar</strong> class Foo { } tun, der Typ von Foo der Rückgabetyp von bar sein sollte. Ich stimme nicht zu, dass this innerhalb Foo in irgendeiner Weise betroffen sein sollte. Die Verwendung connect ist für den Streitpunkt hier völlig irrelevant, da connect nicht erwartet, dass die dekorierte Komponente Member kennt und verwendet, die sie dem Prototyp hinzufügt.

Ich kann verstehen, dass Sie frustriert sind, @masaeedu , aber gehen Sie nicht davon aus, dass ich die Diskussion nicht verfolgt habe, nur weil ich meine Meinung in diesem Hin und Her, das Sie in den letzten 48 Stunden hatten, nicht aktiv geäußert habe - also bitte Erspar mir die Herablassung, Kumpel.

So wie ich es verstehe, denken Sie, dass innerhalb einer dekorierten Klasse keine Mutationen bekannt sein sollten, die von Dekorateuren an sich selbst vorgenommen wurden, aber außerhalb sollten sie es wissen. Dem widerspreche ich nicht. Die Tatsache, dass @arackaf innerhalb einer Klasse denkt, sollte die mutierte Version sehen, ist nebensächlich für meinen Kommentar.

In jedem Fall könnte TypeScript den von einer Decorator-Funktion zurückgegebenen Typ als Quelle der Wahrheit für eine Klasse verwenden. Und daher ist es ein Decorator-Problem, nicht eine neue bizarre Argumentmutationsfunktion, wie Sie vorschlagen, was sich ehrlich gesagt nur wie ein Versuch eines Strohmanns anhört.

@Zalastax , das die Decorator-Funktion als Funktion verwendet, nicht als Decorator. Wenn Sie mehrere Dekorierer haben, die Sie anwenden müssen, müssen Sie sie verschachteln, was technisch nicht mehr ausführlich, aber syntaktisch nicht so angenehm ist - wissen Sie, was ich meine?

Herablassung usw.

Lärm.

Die Tatsache, dass @arackaf innerhalb einer Klasse denkt, sollte die mutierte Version sehen, ist nebensächlich für meinen Kommentar.

Es ist der zentrale Punkt des Absatzes, auf den Sie antworten. Wenn Sie also über etwas anderes sprechen, ist es irrelevant. Was @arackaf will, ist grob gesagt, dass die Schnittstelle mit Funktionsargumenten zusammengeführt wird. Dies wird besser durch ein von Dekorateuren unabhängiges Feature angegangen. Was Sie wollen (was anscheinend identisch mit dem ist, was ich will), ist, den Fehler in TypeScript zu beheben, bei dem <strong i="12">@foo</strong> class Foo { } nicht zu derselben Typsignatur für Foo wie const Foo = foo(class { }) führt.

So wie ich es verstehe, denken Sie, dass innerhalb einer dekorierten Klasse keine Mutationen bekannt sein sollten, die von Dekorateuren an sich selbst vorgenommen wurden, aber außerhalb sollten sie es wissen. Dem widerspreche ich nicht

Wenn Sie sich entschieden haben, über etwas zu sprechen, worüber wir uns einig sind, es aber mit „Ich stimme dieser Aussage nicht zu“ voranstellen, dann verschwenden Sie nur Zeit.

Kann ich mit diesem Salz Pommes bekommen?

Oh, ich verstehe, wenn Sie Ihre Kollegen bevormunden, ist das in Ordnung, aber wenn die Leute Sie darauf anziehen, ist das Lärm.

Ich verstehe nicht, warum Ihr Konzept der "Argumentmutation" benötigt wird, um das umzusetzen, was @arackaf vorgeschlagen hat.

Unabhängig davon, ob wir @arackaf zustimmen oder nicht, könnte TypeScript meiner bescheidenen Meinung nach auf jeden Fall einfach den tatsächlichen Typ aus dem Ergebnis des Dekorators ableiten.

Entschuldigung, wenn Sie sich herablassend fühlten; die Absicht war, dich dazu zu bringen
Eigentlich ein riesiger weitläufiger Thread, der jedem vergeben werden könnte
abschöpfen. Ich stehe dazu, da es immer noch ziemlich klar ist, dass Sie sich dessen nicht bewusst sind
der Details und sind mit mir "nicht einverstanden", basierend auf einem Missverständnis von
meine Position.

Zum Beispiel Ihre Frage, warum die Schnittstellenzusammenführung funktioniert
Argumente lösen Adams Problem lässt sich am besten beantworten, indem man sich das Relevante ansieht
Diskussion mit Adam, wo er sagt, dass er bereits Interface-Merging verwendet,
findet es aber lästig, dies an jeder Deklarationsstelle tun zu müssen.

Ich denke, alle technischen Aspekte davon wurden totgeschlagen. ich
Ich möchte nicht, dass der Thread von zwischenmenschlichen Streitereien dominiert wird, also das
wird mein letzter Beitrag.

Es stimmt, es gibt zwei Fehler bei Decorators: der Rückgabetyp wird nicht respektiert, und klassenintern hinzugefügte Mitglieder werden nicht respektiert ( this innerhalb der Klasse). Und ja, ich mache mir mehr Sorgen um letzteres, da ersteres leichter zu umgehen ist.

Ich habe hier einen separaten Fall geöffnet: https://github.com/Microsoft/TypeScript/issues/16599

Hallo zusammen, ich habe dieses Gespräch über das Wochenende verpasst, und vielleicht gibt es keinen Grund, es jetzt wieder hochzubringen; Aber ich möchte jeden daran erinnern, dass in der Hitze dieser Art von Diskussionen jeder, der sich einmischt, normalerweise gute Absichten hat. Ein respektvoller Ton kann helfen, das Gespräch zu klären und zu lenken, und kann neuere Mitwirkende fördern. Dies ist nicht immer einfach (besonders wenn das Internet dem Leser die Tonalität überlässt), aber ich denke, es ist wichtig für zukünftige Szenarien. 😃

Wie ist der Stand diesbezüglich?

@alex94puchades es ist immer noch ein Vorschlag für Phase 2 , also haben wir wahrscheinlich noch eine Weile Zeit. TC39 scheint zumindest etwas Bewegung zu haben .

Laut diesem Kommentar sieht es so aus, als könnte es bereits im November für Stufe 3 vorgeschlagen werden.

eine Problemumgehung, um die Signatur einer Funktion durch den Dekorateur zu ändern

Fügen Sie eine leere Wapper-Funktion hinzu

export default function wapper (cb: any) {
    return cb;
}

Definition hinzufügen

export function wapper(cb: IterableIterator<0>): Promise<any>;

Ergebnis

<strong i="13">@some</strong> decorator // run generator and return promise
function *abc() {}

wapper(abc()).then() // valid

/Klingeln

Wenn jemand nach einer Lösung dafür sucht, ist eine Problemumgehung, die ich mir ausgedacht habe, unten.

Es ist nicht die beste Lösung, weil es ein verschachteltes Objekt erfordert, aber für mich funktioniert es gut, weil ich eigentlich wollte, dass sich die Eigenschaften des Dekorators in einem Objekt befinden und nicht nur flach auf der Klasseninstanz.

Dies ist etwas, das ich für meine Angular 5-Modals verwende.

Stellen Sie sich vor, ich hätte einen @ModalParams(...) Decorator, den ich für benutzerdefinierte ConfirmModalComponent verwenden kann. Damit die Eigenschaften des @ModalParams(...) -Dekorators in meiner benutzerdefinierten Komponente angezeigt werden, muss ich eine Basisklasse erweitern, die eine Eigenschaft hat, der der Dekorator seine Werte zuweist.

Zum Beispiel:

export class Modal {
    params: any;

    constructor(values: Object = {}) {
        Object.assign(this, values);
    }
}

export function ModalParams (params?: any) {
    return (target: any): void  => {
        Object.assign(target.prototype, {
            params: params
        });
    };
}

@Component({...})
@ModalOptions({...})
@ModalParams({
    width:             <number> 300,
    title:             <string> 'Confirm',
    message:           <string> 'Are you sure?',
    confirmButtonText: <string> 'Yes',
    cancelButtonText:  <string> 'No',
    onConfirm:         <(modal: ConfirmModalComponent) => void> (() => {}),
    onCancel:          <(modal: ConfirmModalComponent) => void> (() => {})
})
export class ConfirmModalComponent extends Modal {
    constructor() {
        super();
    }

    confirm() {
        this.params.onConfirm(this); // This does not show a syntax error 
    }

    cancel() {
        this.params.onCancel(this); // This does not show a syntax error 
    }
}

Auch hier ist es nicht besonders hübsch, aber es funktioniert gut für meinen Anwendungsfall, also dachte ich, jemand anderes könnte es nützlich finden.

@lansana aber du verstehst die typen nicht oder?

@confraria Leider nicht, aber es gibt möglicherweise eine Möglichkeit, dies zu erreichen, wenn Sie eine generische Modal -Klasse implementieren, die Sie erweitern. Zum Beispiel könnte so etwas funktionieren (nicht getestet):

export class Modal<T> {
    params: T;
}

export function ModalParams (params?: any) {
    return (target: any): void  => {
        Object.assign(target.prototype, {
            params: params
        });
    };
}

// The object in @ModalParams() should be of type MyType
@ModalParams({...})
export class ConfirmModalComponent extends Modal<MyType> {
    constructor() {
        super();
    }
}

:/ yup, aber dann ist der Typ vom Decorator entkoppelt und Sie können sicherlich nicht zwei davon verwenden..:( Außerdem würden Sie Fehler bekommen, wenn die Klasse die Methoden nicht implementieren würde.. :( ich glaube nicht ist im Moment eine Möglichkeit, die Typen mit diesem Muster richtig hinzubekommen

Ja, es wäre großartig, wenn dies möglich wäre, macht TypeScript so viel besser und ausdrucksstarker. Hoffentlich wird daraus bald etwas.

@lansana Ja, der Punkt ist, dass es für Klassendekorateure schön wäre, die Signatur der Klasse selbst ändern zu können, ohne dass die Klasse etwas anderes erweitern oder implementieren muss (da dies eine Verdoppelung von Aufwand und Typinformationen ist). .

Nebenbemerkung: Denken Sie in Ihrem Beispiel daran, dass params über alle Instanzen von dekorierten modalen Komponentenklassen hinweg statisch wäre, da es sich um eine Objektreferenz handelt. Obwohl das vielleicht beabsichtigt ist. : - ) Aber ich schweife ab...

Bearbeiten: Wenn ich darüber nachdenke, sehe ich die Nachteile, wenn es Dekoratoren ermöglicht wird, Klassensignaturen zu ändern. Wenn die Klassenimplementierung eindeutig bestimmte Typanmerkungen enthält, aber ein Dekorateur in der Lage ist, all das zu ändern, wäre das ein ziemlich gemeiner Trick, den man Entwicklern antun könnte. Viele der Anwendungsfälle betreffen offensichtlich die Einbindung neuer Klassenlogik, daher würden viele von ihnen leider besser durch eine Erweiterung oder Schnittstellenimplementierung erleichtert werden - was auch mit der vorhandenen Klassensignatur koordiniert ist und bei Kollisionen entsprechende Fehler auslöst. Ein Framework wie Angular macht natürlich reichlich Gebrauch von Decorators, um Klassen zu erweitern, aber das Design soll es der Klasse nicht ermöglichen, neue Logik vom Decorator zu "gewinnen" oder einzumischen, die sie dann in ihrer eigenen Implementierung verwenden kann - es soll isolieren Klassenlogik von der Framework-Koordinierungslogik. Das ist sowieso meine _bescheidene_ Meinung. : - )

Es scheint besser zu sein, nur Klassen höherer Ordnung zu verwenden als Decorators + Hacks. Ich weiß, dass die Leute Dekorateure für dieses Zeug verwenden wollen, aber die Verwendung von Compose + HOCs ist der richtige Weg und wird wahrscheinlich so bleiben ... für immer ;) Laut MS usw. sind Dekorateure nur zum Anhängen von Metadaten an Klassen und wenn Sie Überprüfen Sie große Benutzer von Dekorateuren wie Angular, Sie werden sehen, dass sie nur in dieser Funktion verwendet werden. Ich bezweifle, dass Sie die TypeScript-Betreuer vom Gegenteil überzeugen können.

Es ist traurig und ein bisschen seltsam, dass ein so mächtiges Feature, das eine echte Feature-Komposition ermöglicht und ein solches Engagement erzeugt, vom TS-Team so lange ignoriert wurde.

Dies ist wirklich ein Feature, das die Art und Weise, wie wir Code schreiben, revolutionieren könnte; Es ermöglicht jedem, kleine Mixins für Dinge wie Bitly oder NPM zu veröffentlichen und eine wirklich großartige Code-Wiederverwendung in Typescript zu haben. In meinen eigenen Projekten würde ich sofort mein @Poolable @Initable @Translating und wahrscheinlich einen Haufen mehr machen.

Bitte alles mächtige TS-Kernteam. "Alles, was Sie zur Implementierung benötigen", ist, dass die zurückgegebenen Schnittstellen berücksichtigt werden sollten.

// taken from my own lib out of context
export function Initable<T extends { new(...args: any[]): {} }>(constructor: T): T & Constructor<IInitable<T>> {
    return class extends constructor implements IInitable<T> {
        public init(obj: Partial<T> | any, mapping?: any) {
            setProperties(this, obj, mapping);
            return this
        }
    }
}

wodurch dieser Code ohne Beanstandung ausgeführt werden kann:

<strong i="14">@Initable</strong>
class Person {
    public name: string = "";
    public age: number = 0;
    public superPower: string | null = null;
}
let sam = new Person();

sam.init({age: 17, name: "Sam", superPower: "badassery"});

@AllNamesRTaken

Obwohl ich Ihren Punkten zustimme, können Sie dasselbe wie folgt erreichen:

class Animal {
    constructor(values: Object = {}) {
        Object.assign(this, values);
    }
}

Und dann bräuchten Sie nicht einmal eine Init-Funktion, Sie könnten einfach Folgendes tun:

const animal = new Animal({name: 'Fred', age: 1});

@Lansana
Ja, aber das würde meinen Konstruktor verschmutzen, was nicht immer super toll ist, was ich will.

Außerdem habe ich ein ähnliches Mixin, um jedes Objekt Poolable und andere Dinge zu machen. Es wird lediglich die Möglichkeit benötigt, Funktionalität durch Komposition hinzuzufügen. Das Init-Beispiel ist nur ein notwendiger Weg, um das zu tun, was Sie getan haben, ohne den Konstruktor zu verunreinigen, wodurch es mit anderen Frameworks nützlich wird.

@AllNamesRTaken Es macht derzeit keinen Sinn, Dekorateure zu kontaktieren, da es sich schon so lange in diesem Zustand befindet und der Vorschlag bereits in Phase 2 ist. Warten Sie, bis https://github.com/tc39/proposal-decorators abgeschlossen ist, und dann werden wir sie höchstwahrscheinlich in der Form erhalten, auf die sich alle einigen.

@Kukkimonsuta
Ich stimme Ihnen entschieden nicht zu, da es bei meiner Bitte nur um den Typ und nicht um die Funktionalität geht. Daher hat es nicht viel, wenn überhaupt, mit ES-Zeug zu tun. Meine obige Lösung funktioniert bereits, ich möchte nur nicht umwandeln müssen.

@AllNamesRTaken können Sie dies _heute_ mit Mixins ohne jegliche Warnungen tun:

function setProperties(t: any, o: any, mapping: any) {}

type Constructor<T> = { new(...args: any[]): T };

interface IInitable<T> {
  init(obj: Partial<T> | any, mapping?: any): this;
}

// taken from my own lib out of context
function Initable<T extends Constructor<{}>>(constructor: T): T & Constructor<IInitable<T>> {
    return class extends constructor implements IInitable<T> {
        public init(obj: Partial<T> | any, mapping?: any) {
            setProperties(this, obj, mapping);
            return this
        }
    }
}

class Person extends Initable(Object) {
    public name: string = "";
    public age: number = 0;
    public superPower: string | null = null;
}
let sam = new Person();
sam.init({age: 17, name: "Sam", superPower: "badassery"});

Wenn wir in Zukunft den Mixins-Vorschlag in JS erhalten, können wir Folgendes tun:

mixin Initable {
  public init(obj: Partial<T> | any, mapping?: any) {
    setProperties(this, obj, mapping);
    return this
  }
}

class Person extends Object with Initable {
    public name: string = "";
    public age: number = 0;
    public superPower: string | null = null;
}
let sam = new Person();
sam.init({age: 17, name: "Sam", superPower: "badassery"});

@justinfagnani mixin war, wie ich mit diesen Features angefangen habe und meine Implementierung funktioniert tatsächlich auch so, wie du es beschreibst:

class Person extends Initable(Object) {
    public name: string = "";
    public age: number = 0;
    public superPower: string | null = null;
}
let sam = new Person();     
sam.init({age: 17, name: "Sam", superPower: "badassery"});

was nett ist, als Problemumgehung, aber meiner Meinung nach nicht wirklich die Vorzüge beseitigt, Dekorateuren zu erlauben, den Typ zu ändern.

BEARBEITEN: Außerdem verliert es die Eingabe des Teils für init.

Mit dieser Funktion können Sie HoC einfacher schreiben
Ich hoffe, dass diese Funktion hinzugefügt wird

@kgtkr das ist der Hauptgrund, warum ich das so sehr will ...

Es gibt auch einen kleinen Notfall in react-router -Definitionen, weil einige Leute entschieden haben, dass Typensicherheit wichtiger ist als Klassendekorationsschnittstellen zu haben. Der Grund dafür ist, dass withRouter einige der Requisiten optional macht.

Jetzt scheint es eine Fehde zu geben, bei der die Leute gezwungen sind, die Funktionsschnittstelle von withRouter anstelle von Dekoration zu verwenden .

Diese Funktion zu lösen, würde die Welt glücklicher machen.

Die gleiche Fehde* ereignete sich vor einiger Zeit bei den Material- UI-Typisierungen und withStyle , die als Dekorierer gedacht waren, aber TypeScript-Benutzer verwenden müssen, um sie typsicher verwenden zu können als regelmäßige Funktion. Auch wenn sich der Staub darauf größtenteils gelegt hat, ist es eine Quelle anhaltender Verwirrung für Neueinsteiger in das Projekt!

* Nun, "Fehde" ist vielleicht ein starkes Wort

Ich beobachte das schon lange und bis es soweit ist, können andere hoffentlich von meinem kleinen Hack profitieren, um dieses Verhalten aufwärtskompatibel zu erreichen. Hier ist es also, die Implementierung einer supereinfachen Methode der Fallklasse copy im Scala-Stil ...

Ich habe den Decorator so implementiert:

type Constructor<T> = { new(...args: any[]): T };

interface CaseClass {
  copy(overrides?: Partial<this>): this
}

function CaseClass<T extends Constructor<{}>>(constructor: T): T & Constructor<CaseClass> {
  return class extends constructor implements CaseClass {
    public copy(overrides: Partial<this> = {}): this {
      return Object.assign(Object.create(Object.getPrototypeOf(this)), this, overrides);
    }
  }
}

Dieser Code erstellt eine anonyme Klasse mit angehängter copy -Methode. Es funktioniert genau wie in JavaScript erwartet, wenn es sich in einer Umgebung befindet, die Dekorateure unterstützt.

Um dies in TypeScript zu verwenden und das Typsystem tatsächlich die neue Methode in der Zielklasse widerspiegeln zu lassen, kann der folgende Hack verwendet werden:

class MyCaseClass extends CaseClass(class {
  constructor(
    public fooKey: string,
    public barKey: string,
    public bazKey: string
  ) {}
}) {}

Alle Instanzen von MyCaseClass machen die Eingaben für die geerbte copy -Methode von der anonymen Klasse innerhalb des CaseClass -Dekorators verfügbar. Und wenn TypeScript die Mutation von deklarierten Typen unterstützt, kann dieser Code ohne unerwartete Aussetzer einfach in die übliche Decorator-Syntax <strong i="18">@CaseClass</strong> etc geändert werden.

Es wäre großartig, dies in der nächsten großen TypeScript-Version zu sehen - ich glaube, dass es zu einem saubereren und prägnanteren Code beitragen wird, anstatt ein seltsames Durcheinander von Proxy-Klassen zu exportieren.

Wann wird diese Funktion verfügbar sein?

Ich wollte einen Klassendekorateur verwenden, um sich um wiederholte Aufgaben zu kümmern.
Aber jetzt, wenn ich die Klasse initialisiere, erhalte ich die folgende Fehlermeldung: Expected 0 arguments, but got 1

function Component<T extends { new(...args: any[]): {} }>(target: T) {
    return class extends target {
        public constructor(...args: any[]) {
            super(...args);
            resolveDependencies(this, args[0])
        }
    }
}

<strong i="8">@Component</strong>
export class ExampleService {
    @Inject(ExampleDao) private exampleDao: ExampleDao;

    // <strong i="9">@Component</strong> will automatically do this for me 
    // public constructor(deps: any) {
    //  resolveDependencies(this, deps);
    // }

    public getExample(id: number): Promise<Example | undefined> {
        return this.exampleDao.getOne(id);
    }
}

new ExampleService({ exampleDao }) // TS2554: Expected 0 arguments, but got 1.

Ich hoffe, dass diese Funktion bald verfügbar sein wird! :)

@iainreid820 sind bei diesem Ansatz seltsame Fehler aufgetreten?

Das lange Warten! Wird dies in der Zwischenzeit durch irgendetwas auf der aktuellen Roadmap gelöst?
Zum Beispiel Ausgabe 5453 ?

Ich verwende Material UI und musste feststellen, dass TypeScript die Decorator-Syntax für withStyles nicht unterstützt: https://material-ui.com/guides/typescript/#decorating -components

Bitte beheben Sie diese Einschränkung in der nächsten TypeScript-Version. Klassendekorateure scheinen mir im Moment ziemlich nutzlos zu sein.

Als Betreuer von Morphism Js ist dies eine große Einschränkung für diese Art von Bibliothek. Ich würde gerne vermeiden, dass der Verbraucher eines Funktionsdekorators den Zieltyp der Funktion https://github.com/nobrainr/morphism# --toclassobject-decorator angeben muss, ansonsten klingt die Verwendung von Dekoratoren anstelle von HOF etwas nutzlos 😕
Gibt es einen Plan, dies anzugehen? Gibt es eine Möglichkeit, diese Funktion zu ermöglichen? Vielen Dank im Voraus!

@bikeshedder , dass das Material-UI-Beispiel ein Klassen-Mixin-Anwendungsfall ist, und Sie können den richtigen Typ von Mixins erhalten. Anstatt:

const DecoratedClass = withStyles(styles)(
  class extends React.Component<Props> {
...
}

schreiben:

class DecoratedClass extends withStyles(styles)(React.Component<Props>) {
...
}

@justinfagnani Das funktioniert bei mir nicht:

ss 2018-10-16 at 10 00 47

Hier ist mein Code: https://gist.github.com/G-Rath/654dff328dbc3ae90d16caa27a4d7262

@G-Rath Ich denke, new () => React.Component<Props, State> sollte stattdessen funktionieren?

@emyann Keine Würfel. Ich habe das Wesentliche mit dem neuen Code aktualisiert, aber ist das gemeint?

class CardSection extends withStyles(styles)(new () => React.Component<Props, State>) {

Egal wie Sie es formatieren, extends withStyles(styles)(...) klingt grundsätzlich nicht wie ein richtiger Vorschlag. withStyles ist KEIN Klassen-Mixin.

withStyles nimmt die Komponentenklasse A und erstellt die Klasse B , die beim Rendern die Klasse A rendert und Requisiten an sie + die Requisite classes übergibt.

Wenn Sie den Rückgabewert von withStyles erweitern, erweitern Sie den Wrapper B , anstatt die Klasse A zu implementieren, die tatsächlich die Eigenschaft classes empfängt.

Egal wie Sie es formatieren, Erweiterungen mit Styles (Stile) (...) klingen im Grunde nicht nach einem richtigen Vorschlag. withStyles ist KEIN Klassen-Mixin.

In der Tat. Ich bin mir nicht sicher, warum es hier so viel Gegenwind gibt.

Allerdings sind Dekorateure sehr nah an Stufe 3, wie ich höre, also gehe ich davon aus, dass TS hier bald die richtige Unterstützung bekommen wird.

Ich höre die Leute, die eine sauberere Syntax wollen, insb. bei Verwendung mehrerer HoCs als Decorators. Die vorübergehende Problemumgehung, die wir verwenden, besteht darin, mehrere Dekorierer tatsächlich in eine Pipe-Funktion einzuschließen und dann diese reine Funktion zum Dekorieren der Komponente zu verwenden. z.B

@flow([
  withStyles(styles),
  connect(mapStateToProps),
  decorateEverything(),
])
export class HelloWorld extends Component<Props, State> {
  ...
}

wobei flow lodash.flow ist. Viele Dienstprogrammbibliotheken bieten so etwas an - recompose , Rx.pipe usw., aber Sie können natürlich Ihre eigene einfache Pipe-Funktion schreiben, wenn Sie keine Bibliothek installieren möchten.

Ich finde das einfacher zu lesen, als keine Dekorateure zu verwenden,

export const decoratedHelloWorld = withStyles(styles)(
  connect(mapStateToProps)(
    decorateEverything(
       HelloWorld
))))

Ein weiterer Grund, warum ich dies verwende, ist, dass dieses Muster leicht zu finden und zu ersetzen/grep-fähig ist, sobald die Decorator-Spezifikation angekündigt und ordnungsgemäß unterstützt wird.

@justinfagnani Seltsamerweise erhalte ich einen Syntaxfehler von ESLint, wenn ich versuche, extends React.Component<Props, State> in extends withStyles(styles)(React.Component<Props, State>) zu ändern, sodass ich den Typfehler von @G-Rath nicht auf die gleiche Weise überprüfen konnte.

Aber ich habe bemerkt, dass, wenn ich etwas wie folgt mache, ein Problem mit new (vielleicht das gleiche Problem?) erscheint:

class MyComponent extends React.Component<Props, State> {
  /* ... */
}

const _MyComponent = withStyles(styles)(MyComponent)
const test = new _MyComponent // <--------- ERROR

und der fehler ist:

Cannot use 'new' with an expression whose type lacks a call or construct signature.

Bedeutet dies, dass der von Material UI zurückgegebene Typ kein Konstruktor ist (obwohl er einer sein sollte)?

@sagar-sm Aber erhalten Sie den richtigen Typ, der durch den Material-UI-Typ erweitert wird? Scheint nicht so, als würdest du es tun.

Als Referenz bin ich darüber gestolpert, weil ich auch versucht habe, withStyles von Material UI als Decorator zu verwenden, was nicht funktioniert hat, also habe ich diese Frage gestellt: https://stackoverflow.com/questions/53138167

brauchen, dann können wir die asynchrone Funktion als Bluebird zurückgeben lassen

Ich habe versucht, etwas Ähnliches zu tun. ATM Ich verwende folgende Problemumgehung:

Zuerst ist hier mein "Mixin"-Dekorateur

export type Ctor<T = {}> = new(...args: any[]) => T 
function mixinDecoratorFactory<MixinInterface>() {
    return function(toBeMixed: MixinInterface) {
        return function<MixinBase extends Ctor>(MixinBase: MixinBase) {
            Object.assign(MixinBase.prototype, toBeMixed)
            return class extends MixinBase {} as MixinBase & Ctor<MixinInterface>
        }
    }
}

Erstellen Sie einen Dekorateur aus der Schnittstelle

export interface ComponentInterface = {
    selector: string,
    html: string
}
export const Component = mixinDecoratorFactory<ComponentInterface>();

Und so benutze ich es:

@Component({
    html: "<div> Some Text </div>",
    selector: "app-test"
})
export class Test extends HTMLElement {
    test = "test test"
    constructor() {
        super()
        console.log("inner;     test:", this.test)
        console.log("inner;     html:", this.html)
        console.log("inner; selector:", this.selector)
    }
}

export interface Test extends HTMLElement, ComponentInterface {}
window.customElements.define(Test.prototype.selector, Test)


const test = new Test();
console.log("outer;     test:", test.test)
console.log("outer;     html:", test.html)
console.log("outer; selector:", test.selector)

Der Trick besteht darin, auch eine Schnittstelle mit demselben Namen wie die Klasse zu erstellen, um eine zusammengeführte Deklaration zu erstellen.
Die Klasse wird immer noch nur als Typ Test angezeigt, aber Überprüfungen aus Typoskript funktionieren.
Wenn Sie den Dekorator ohne die @ -Notation verwenden und ihn einfach als Funktion aufrufen würden, würden Sie den richtigen Schnittpunkttyp erhalten, aber die Fähigkeit zur Typprüfung innerhalb der Klasse selbst verlieren, da Sie die nicht verwenden können Interface-Trick mehr und es sieht hässlicher aus. Z.B:

let Test2Comp = Component({
    html: "<div> Some Text 2 </div>",
    selector: "app-test2"
}) (
class Test2 extends HTMLElement {
    test = "test test"
    constructor() {
        super()
        console.log("inner;     test:", this.test)
        console.log("inner;     html:", this.html)     // no
        console.log("inner; selector:", this.selector) // no
    }
})

interface Test2 extends HTMLElement, ComponentInterface {} //no
window.customElements.define(Test2Comp.prototype.selector, Test2Comp)

const test2 = new Test2Comp();
console.log("outer;     test:", test2.test)
console.log("outer;     html:", test2.html)
console.log("outer; selector:", test2.selector)

Was sagen Sie zu diesen Ansätzen? Es ist nicht schön, aber funktioniert so weit wie ein Workaround.

Gibt es Fortschritte bei diesem Thema? Dies scheint eine sehr, sehr mächtige Funktion zu sein, die eine ganze Menge verschiedener Möglichkeiten freisetzen würde. Ich nehme an, diese Ausgabe ist im Moment größtenteils veraltet, weil Dekorateure noch experimentieren?

Wäre wirklich toll, ein offizielles Statement zu diesem @andy-ms @ahejlsberg @sandersn zu haben. 🙏

Wir werden hier wahrscheinlich nichts tun, bis die Dekorateure fertig sind.

@DanielRosenwasser - was heißt hier "abgeschlossen"? Qualifiziert sich Stufe 3 in TC39?

Ich wusste nicht, dass sie so weit gekommen waren! Wann haben sie Stufe 3 erreicht? Ist das neu?

Das haben sie noch nicht; Ich glaube, sie stehen beim TC39-Meeting im Januar wieder für den Aufstieg von Stufe 2 zu Stufe 3 bereit.

Einzelheiten können Sie der Tagesordnung entnehmen.

Richtig (obwohl ich nicht bestätigen kann, dass es im Januar zur Beförderung ansteht). Ich habe nur gefragt, ob sich Phase 3 qualifizieren würde, wenn sie dort ankommen.

Meiner Meinung nach hat der Decorator-Vorschlag noch viele ernsthafte Probleme. Wenn er also im Januar in Phase 3 vordringt, wird er den Fehler erneut machen, genau wie der Vorschlag für problematische Klassenfelder und der globale Vorschlag.

@hax könntest du das näher erläutern?
Ich würde das wirklich sehr wollen und finde den Mangel an Kommunikation in dieser Angelegenheit traurig und habe leider nichts von den Problemen gehört.

@AllNamesRTaken Überprüfen Sie die Problemliste des Dekorateurvorschlags. 😆 Zum Beispiel ist export vor/nach dem Decorator-Argument ein Blocker von Stufe 3.

Es gibt auch einige vorgeschlagene Änderungen wie die Neugestaltung der API, von denen ich glaube, dass der Vorschlag für Stufe 3 nicht stabil ist.

Und es bezog sich auch auf den problematischen Vorschlag für Klassenfelder. Obwohl Klassenfelder Stufe 3 erreicht haben, gibt es zu viele Probleme. Ein großes Problem, das mich betrifft, ist, dass viele Probleme den Dekorateuren überlassen werden (z. B. geschützt) und das ist schlecht für beide Vorschläge.

Beachten Sie, dass ich kein TC39-Delegierter bin und einige TC39-Delegierte niemals meinen Kommentaren zum aktuellen Status vieler Probleme zustimmen. (Besonders bin ich der festen Meinung, dass der aktuelle TC39-Prozess bei vielen kontroversen Themen große Fehler aufweist. Einige Probleme auf die Dekorateure zu verschieben, löst das Problem nie wirklich, macht den Vorschlag der Dekorateure nur anfälliger.)

Ich denke, diese Dinge werden geklärt und der Vorschlag wird in Ordnung sein, aber ich würde es vorziehen, wenn wir den Stand des Vorschlags hier nicht diskutieren würden.

Ich denke, eine Phase 3 mit ausreichendem Vertrauen oder Phase 4 ist wahrscheinlich der Ort, an dem wir den neuen Vorschlag implementieren würden, und dann könnten wir uns mit diesem Thema befassen.

Danke Daniel! Phase 3 impliziert normalerweise ein starkes Vertrauen in 4, also drücken Sie die Daumen für das Januar-Meeting.

Und vielen Dank, dass Sie die Diskussion über die Dekorateure hier geleitet haben. Es ist bizarr, wie viel Ärger und Fahrradabwurf diese Funktion verursacht hat. So etwas habe ich noch nie gesehen 😂

Nur fürs Protokoll, es gab eine Feature-Anfrage zum selben Thema: https://github.com/Microsoft/TypeScript/issues/8545

ärgerlicherweise unterstützt TypeScript diese Funktion bis zu einem gewissen Grad, wenn Sie aus JavaScript kompilieren (https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#better-handling-for-namespace-patterns-in -js-Dateien):

// javascript via typescript
var obj = {};
obj.value = 1;
console.log(obj.value);

und sogar für TypeScript-Code selbst , aber nur, wenn es um Funktionen geht (!)(https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#properties-declarations-on-functions):

function readImage(path: string, callback: (err: any, image: Image) => void) {
    // ...
}

readImage.sync = (path: string) => {
    const contents = fs.readFileSync(path);
    return decodeImageSync(contents);
}

@DanielRosenwasser @arackaf Irgendwas über den Vorschlag, zu Phase 3 überzugehen? Ich möchte eine Bibliothek erstellen, die einer Klasse Prototypfunktionen hinzufügt, wenn ein Dekorateur verwendet wird.

Sie sind beim letzten TC39-Treffen nicht vorgerückt; Sie können beim nächsten Treffen wieder befördert werden. Es ist jedoch unklar; An diesem Punkt liegt viel in der Luft über den Vorschlag.

Leute hier, die daran interessiert sind, wären gut beraten, den Vorschlag selbst zu verfolgen – was den Lärm für diejenigen von uns, die den Thread beobachten, sowie für die TS-Betreuer gering hält.

irgendwelche Updates zu diesem?

Problemumgehung (eckig:)) https://stackblitz.com/edit/iw-ts-extends-with-fakes?file=src%2Fapp%2Fextends-with-fakes.ts

import { Type } from '@angular/core';

export function ExtendsWithFakes<F1>(): Type<F1>;
export function ExtendsWithFakes<F1, F2>(): Type<F1 & F2>;
export function ExtendsWithFakes<F1, F2, F3>(): Type<F1 & F2 & F3>;
export function ExtendsWithFakes<F1, F2, F3, F4>(): Type<F1 & F2 & F3 & F4>;
export function ExtendsWithFakes<F1, F2, F3, F4, F5>(): Type<F1 & F2 & F3 & F4 & F5>;
export function ExtendsWithFakes<RealT, F1>(realTypeForExtend?: Type<RealT>): Type<RealT & F1>;
export function ExtendsWithFakes<RealT, F1, F2>(realTypeForExtend?: Type<RealT>): Type<RealT & F1 & F2>;
export function ExtendsWithFakes<RealT, F1, F2, F3>(realTypeForExtend?: Type<RealT>): Type<RealT & F1 & F2 & F3>;
export function ExtendsWithFakes<RealT, F1, F2, F3, F4>(
    realTypeForExtend?: Type<RealT>
): Type<RealT & F1 & F2 & F3 & F4>;
export function ExtendsWithFakes<RealT, F1, F2, F3, F4, F5>(
    realTypeForExtend?: Type<RealT>
): Type<RealT & F1 & F2 & F3 & F4 & F5> {
    if (realTypeForExtend) {
        return realTypeForExtend as Type<any>;
    } else {
        return class {} as Type<any>;
    }
}

interface IFake {
    fake(): string;
}

function UseFake() {
    return (target: Type<any>) => {
        target.prototype.fake = () => 'hello fake';
    };
}

class A {
    a() {}
}

class B {
    b() {}
}

@UseFake()
class C extends ExtendsWithFakes<A, IFake, B>(A) {
    c() {
        this.fake();
        this.a();
        this.b(); // failed at runtime
    }
}

Was haltet ihr von dieser „inzwischen“ einfachen Lösung?
https://stackoverflow.com/a/55520697/1053872

Mobx-state-tree verwendet einen ähnlichen Ansatz mit seiner Funktion cast() . Es fühlt sich nicht gut an, aber... Es stört mich auch nicht allzu sehr. Es ist einfach herauszunehmen, wenn etwas Besseres kommt.

Ich würde einfach gerne etwas in dieser Richtung machen können ❤️
Ist das nicht die Macht, die Dekorateure haben sollten?

class B<C = any> {}

function ChangeType<T>(to : T) : (from : any) => T;
function InsertType<T>(from : T) : B<T>;

@ChangeType(B)
class A {}

// A === B

// or

<strong i="7">@InsertType</strong>
class G {}

// G === B<G>

const g = new G(); // g : B<G>

A === B // equals true

const a : B = new A();  // valid
const b = new B();

typeof a === typeof b // valid

Ich habe einige Zeit damit verbracht, eine Problemumgehung zu erstellen, um dekorierte Klasseneigenschaften einzugeben. Ich glaube nicht, dass es eine praktikable Lösung für die Szenarien in diesem Thread sein wird, aber einige Teile davon könnten für Sie nützlich sein. Wenn Sie interessiert sind, können Sie meinen Beitrag auf Medium überprüfen

Irgendwelche Updates zu diesem Problem?

Wie geht es uns damit?

Was ist ein Ergebnis dieses Problems, wie funktioniert es jetzt?

Sie können Mixin-Klassen verwenden, um diesen Effekt zu erzielen
https://mariusschulz.com/blog/mixin-classes-in-typescript

@Bnaya , das ist absolut nicht das, was wir jetzt wollen;).
Mit Dekorateuren können wir es vermeiden, Klassen zu erstellen, die wir nie verwenden möchten, wie wir es mit Java der alten Schule tun würden. Dekorateure würden eine sehr saubere und ordentliche Kompositionsarchitektur ermöglichen, in der die Klasse weiterhin Kernfunktionen ausführen und zusätzliche allgemeine Funktionen darauf aufbauen kann. Ja, Mixins könnten es tun, aber es ist Pflaster.

Der Dekorateurvorschlag hat sich in den vergangenen Monaten erheblich geändert - es ist höchst unwahrscheinlich, dass das TS-Team Zeit in eine aktuelle Implementierung investiert, die bald inkompatibel sein wird™.

Ich würde empfehlen, sich die folgenden Ressourcen anzusehen:

Vorschlag für Dekorateure: https://github.com/tc39/proposal-decorators
TypeScript-Roadmap: https://github.com/microsoft/TypeScript/wiki/Roadmap

@Bnaya , das ist absolut nicht das, was wir jetzt wollen;).
Mit Dekorateuren können wir es vermeiden, Klassen zu erstellen, die wir nie verwenden möchten, wie wir es mit Java der alten Schule tun würden. Dekorateure würden eine sehr saubere und ordentliche Kompositionsarchitektur ermöglichen, in der die Klasse weiterhin Kernfunktionen ausführen und zusätzliche allgemeine Funktionen darauf aufbauen kann. Ja, Mixins könnten es tun, aber es ist Pflaster.

Es ist kein Mischen, sondern eine Klasse.
Es ist nicht das Mischen von schlechten Kopien
Wenn der Pipeline-Betreiber eintrifft, wird die Syntax auch angemessen sein

Sie können Mixin-Klassen verwenden, um diesen Effekt zu erzielen

Class-Factory-Mixins können in TypeScript lästig sein; Selbst wenn dies nicht der Fall wäre, sind sie sehr bausteinartig, da Sie Klassen in eine Funktion einschließen müssen, um sie in ein Mixin zu konvertieren, was manchmal nicht einfach möglich ist, wenn Sie Klassen von Drittanbietern importieren.

Das Folgende ist viel besser, einfacher, sauberer und funktioniert mit importierten Klassen von Drittanbietern. Ich habe es in einfachem JavaScript ohne Transpilation gut funktioniert, außer zum Transpilieren des Legacy-Decorators, aber die class es bleiben völlig native class es:

class One {
    one = 1
    foo() { console.log('foo', this.one) }
}

class Two {
    two = 2
    bar() { console.log('bar', this.two) }
}

class Three extends Two {
    three = 3
    baz() { console.log('baz', this.three, this.two) }
}

@with(Three, One)
class FooBar {
    yeah() { console.log('yeah', this.one, this.two, this.three) }
}

let f = new FooBar()

console.log(f.one, f.two, f.three)
console.log(' ---- call methods:')

f.foo()
f.bar()
f.baz()
f.yeah()

Und die Ausgabe in Chrome ist:

1 2 3
 ---- call methods:
foo 1
bar 2
baz 3 2
yeah 1 2 3

Dies gibt Ihnen vollständig das, was Class-Factory-Mixins tun, ohne die ganze Boilerplate. Die Funktionswrapper um Ihre Klassen werden nicht mehr benötigt.

Wie Sie jedoch wissen, kann der Dekorateur den Typ der definierten Klasse nicht ändern. 😢

Also kann ich vorerst Folgendes tun, mit der einzigen Einschränkung, dass geschützte Mitglieder nicht vererbbar sind:

// `multiple` is similar to `@with`, same implementation, but not a decorator:
class FooBar extends multiple(Three, One) {
    yeah() { console.log('yeah', this.one, this.two, this.three) }
}

Das Problem mit dem Verlust geschützter Member wird mit diesen beiden Beispielen der multiple -Implementierungen veranschaulicht (was die Typen betrifft, aber die Laufzeitimplementierung wird der Kürze halber weggelassen):

  • Beispiel ohne Fehler , da alle Eigenschaften in den kombinierten Klassen öffentlich sind.
  • Beispiel mit Fehler (ganz unten), weil die gemappten Typen die geschützten Member ignorieren, wodurch Two.prototype.two in diesem Fall nicht vererbbar wird.

Ich hätte wirklich gerne eine Möglichkeit, Typen einschließlich geschützter Mitglieder abzubilden.

Das wäre wirklich nützlich für mich, weil ich einen Decorator habe, mit dem eine Klasse als Funktion aufgerufen werden kann.

Das funktioniert:

export const MyClass = withFnConstructor(class MyClass {});

Das geht nicht:

<strong i="10">@withFnConstructor</strong>
export class MyClass {}

Eine Problemumgehung, die nicht so schwer zu bereinigen ist, wenn diese Funktion verfügbar ist, kann mithilfe der Zusammenführung von Deklarationen und einer Definitionsdatei für benutzerdefinierte Typen durchgeführt werden.

Habe das schon:

// ClassModifier.ts
export interface Mod {
  // ...
}
// The decorator
export function ClassModifier<Args extends any[], T>(target: new(...args: Args) => T): new(...args: Args) => T & Mod {
  // ...
}
// MyClass.ts
<strong i="9">@ClassModifier</strong>
export MyClass {
  // ...
}

Fügen Sie die folgende Datei hinzu (Datei, die entfernt werden soll, sobald die Funktion herauskommt):

// MyClass.d.ts
import { MyClass } from './MyClass';
import { Mod } from './ClassModifier';

declare module './MyClass' {
  export interface MyClass extends Mod {}
}

Eine weitere mögliche Problemumgehung

declare class Extras { x: number };
this.Extras = Object;

class X extends Extras {
   constructor() {  
      super(); 
      // a modification to object properties that ts will not pick up
      Object.defineProperty(this, 'x', {value: 3});
   }
}

const a = new X()
a.x // 3

Begonnen im Jahr 2015 und noch ausstehend. Es gibt viele weitere verwandte Probleme und sogar Artikel wie diesen https://medium.com/p/caf24aabcb59/responses/show , in denen versucht wird, Problemumgehungen aufzuzeigen (die etwas hacky, aber hilfreich sind).

Ist es möglich, dass jemand etwas Licht ins Dunkel bringt. Wird es schon für eine interne Diskussion in Erwägung gezogen?

tl;dr - diese Funktion ist in TC39 blockiert. Das kann man den TS-Leuten überhaupt nicht vorwerfen. Sie werden nicht implementieren, bis es Standard ist.

Aber das ist Microsoft, sie können es einfach implementieren und dann zu den Standardleuten sagen - "sehen Sie, die Leute verwenden bereits unsere Version" :trollface:

Das Problem hier ist, dass sich der Vorschlag für Dekoratoren _erheblich_ (auf abwärtsinkompatible Weise) geändert hat, seit TypeScript Dekoratoren implementiert hat. Dies wird zu einer schmerzhaften Situation führen, wenn die neuen Dekoratoren fertiggestellt und implementiert werden, da sich einige Leute auf das aktuelle Verhalten und die Semantik der Dekoratoren von TypeScript verlassen. Ich vermute stark, dass das TypeScript-Team Maßnahmen gegen dieses Problem vermeiden möchte, da dies die weitere Verwendung der aktuellen Nicht-Standard-Decorators fördern würde, was letztendlich den Übergang zu einer neuen Decorator-Implementierung noch schmerzhafter machen würde. Im Grunde sehe ich persönlich das einfach nicht.

Übrigens habe ich kürzlich #36348 erstellt, was ein Vorschlag für eine Funktion ist, die eine ziemlich solide Problemumgehung bieten würde. Fühlen Sie sich frei, einen Blick darauf zu werfen/Feedback zu geben/zu stöbern. 🙂

tl;dr - diese Funktion ist in TC39 blockiert. Das kann man den TS-Leuten überhaupt nicht vorwerfen. Sie werden nicht implementieren, bis es Standard ist.

Angesichts dieser Tatsache könnte es für das Entwicklungsteam ratsam sein, eine schnelle Erklärung und Statusaktualisierung zu verfassen und diese Konversation zu sperren, bis TC39 dies in die erforderliche Phase bringt?

Hat jemand, der dies liest, irgendein Gewicht in der Angelegenheit?

Eigentlich hängt so ziemlich jedes Gespräch über Dekorateure in der Luft. Beispiel: https://github.com/Microsoft/TypeScript/issues/2607

Es wird hilfreich sein, etwas Klarheit über die Zukunft der Dekorateure zu bekommen, auch wenn es darum geht, Mitwirkende zu bitten, die erforderliche Funktionalität zu implementieren. Ohne klare Anleitung ist die Zukunft der Dekorateure verschwommen

Ich würde es lieben, wenn das TypeScript-Team eine proaktive Rolle für den Decorator-Vorschlag übernehmen könnte, ähnlich wie sie dazu beigetragen haben, das optionale Verketten durchzubringen. Ich würde auch gerne helfen, habe aber noch nicht in der Sprachentwicklung gearbeitet und bin mir daher unsicher, wie ich das am besten mache :-/

Ich kann nicht für das gesamte Team und TypeScript als Projekt sprechen – aber ich denke, es ist unwahrscheinlich, dass wir neue Decorator-Arbeiten durchführen werden, bis es geklärt und bestätigt ist, da die Decorator-Implementierung bereits einmal von der TypeScript-Implementierung abgewichen ist.

Der Vorschlag befindet sich noch in der aktiven Entwicklung und wurde diese Woche im TC39 veröffentlicht https://github.com/tc39/proposal-decorators/issues/305

@orta in diesem Fall denke ich, dass die Dokumentation zum Klassendekorateur dann aktualisiert werden sollte. Es sagt, dass

Wenn der Klassen-Decorator einen Wert zurückgibt, ersetzt er die Klassendeklaration durch die bereitgestellte Konstruktorfunktion.

Außerdem scheint das folgende Beispiel aus der Dokumentation zu implizieren, dass der Instanztyp Greeter die Eigenschaft newProperty hätte, was nicht stimmt:

function classDecorator<T extends {new(...args:any[]):{}}>(constructor:T) {
    return class extends constructor {
        newProperty = "new property";
        hello = "override";
    }
}

<strong i="13">@classDecorator</strong>
class Greeter {
    property = "property";
    hello: string;
    constructor(m: string) {
        this.hello = m;
    }
}

console.log(new Greeter("world"));

Ich denke, es lohnt sich, den Dokumenten hinzuzufügen, dass die Schnittstelle der zurückgegebenen Klasse die ursprüngliche nicht ersetzen wird.

Interessant, ich habe eine Reihe alter Versionen von TypeScript getestet und konnte keine Gelegenheit finden, bei der dieses Codebeispiel funktionierte ( Beispiel aus 3.3.3 ) - also ja, ich stimme zu.

Ich habe https://github.com/microsoft/TypeScript-Website/issues/443 erstellt – wenn jemand weiß, wie dieser classDecorator einen expliziten Schnittpunkttyp hat, kommentieren Sie bitte in dieser Ausgabe

Ich bin hier gelandet, weil mich das Problem mit der oben aufgeführten Dokumentation verwirrt hat. Obwohl es großartig wäre, wenn die Typsignatur des vom Dekorateur zurückgegebenen Werts vom TS-Compiler erkannt würde, stimme ich zu, dass anstelle dieser größeren Änderung die Dokumentation geklärt werden sollte.

Fürs Protokoll, dies ist eine vereinfachte Version dessen, was ich versucht habe, in einem Format wie die zugehörigen Dokumente aus @ortas PR:

interface HasNewProperty {
  newProperty: string;
}

function classDecorator<T extends { new (...args: any[]): {} }>(
  constructor: T
) {
  return class extends constructor implements HasNewProperty {
    newProperty = "new property";
    hello = "override";
  };
}

<strong i="8">@classDecorator</strong>
class Greeter {
  property = "property";
  hello: string;
  constructor(m: string) {
    this.hello = m;
  }
}

console.log(new Greeter("world"));

// Alas, this line makes the compiler angry because it doesn't know
// that Greeter now implements HasNewProperty
console.log(new Greeter("world").newProperty);
War diese Seite hilfreich?
0 / 5 - 0 Bewertungen