Typescript: Demande de fonctionnalité : mutation de décorateur de classe

Créé le 20 sept. 2015  ·  231Commentaires  ·  Source: microsoft/TypeScript

Si nous pouvions obtenir une vérification de type correcte, nous aurions un support parfait pour les mixins sans passe-partout :

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

Commentaire le plus utile

La même chose serait utile pour les méthodes :

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

Le décorateur asynchrone est censé changer le type de retour en Promise<any> .

Tous les 231 commentaires

La même chose serait utile pour les méthodes :

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

Le décorateur asynchrone est censé changer le type de retour en Promise<any> .

@Gaelan , c'est exactement ce dont nous avons besoin ici ! Cela rendrait les mixins tout simplement naturels à utiliser.

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 pour ça. Bien que je sache que c'est difficile à mettre en œuvre, et probablement plus difficile de parvenir à un accord sur la sémantique de la mutation du décorateur.

+1

Si le principal avantage de ceci est d'introduire des membres supplémentaires dans la signature de type, vous pouvez déjà le faire avec la fusion d'interface :

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

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

new Foo().foo();

Si le décorateur est une fonction réelle que le compilateur doit invoquer pour muter impérativement la classe, cela ne semble pas être une chose idiomatique à faire dans un langage de type sécurisé, à mon humble avis.

@masaeedu Connaissez-vous une solution de contournement pour ajouter un membre statique à la classe décorée ?

@davojan Bien sûr. Voici:

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

Il serait également utile de pouvoir introduire des propriétés _multiples_ dans une classe lors de la décoration d'une méthode (par exemple, un assistant qui génère un setter associé pour un getter, ou quelque chose dans ce sens)

Les typages react-redux pour connect prennent un composant et renvoient un composant modifié dont les accessoires n'incluent pas les accessoires connectés reçus via redux, mais il semble que TS ne reconnaisse pas leur définition connect comme un décorateur à cause de ce problème. Quelqu'un at-il une solution de contournement?

Je pense que la définition de type ClassDecorator doit être modifiée.

Actuellement, c'est declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void; . Peut-être pourrait-il être changé en

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;

Évidemment, le nommage est nul et je n'ai aucune idée si ce genre de chose fonctionnera (j'essaie juste de convertir une application Babel en texte dactylographié et je frappe dessus).

@joyt Pourriez-vous fournir une reconstitution du terrain de jeu du problème ? Je n'utilise pas react-redux, mais comme je l'ai déjà mentionné, je pense que toutes les extensions que vous souhaitez pour la forme d'un type peuvent être déclarées à l'aide de la fusion d'interface.

@masaeedu voici une ventilation de base des pièces mobiles ..

Fondamentalement, le décorateur fournit un ensemble d'accessoires au composant React, de sorte que le type générique du décorateur est un sous-ensemble du composant décoré, pas un sur-ensemble.

Je ne sais pas si cela est utile, mais j'ai essayé de créer un échantillon non exécutable pour vous montrer les types en jeu.

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

Si vous voulez un exemple de travail complet, je vous suggère de supprimer https://github.com/jaysoo/todomvc-redux-react-typescript ou un autre exemple de projet react/redux/typescript.

Selon https://github.com/wycats/javascript-decorators#class -declaration, je crois comprendre que le declare type WrappingClassDecorator = <TFunction extends Function, TDecoratorFunction extends Function>(target: TFunction) => TDecoratorFunction; proposé n'est pas valide.

La spécification dit:

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

se traduit par :

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

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

Donc, si j'ai bien compris, ce qui suit devrait être vrai:

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

Pour votre exemple :

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

Je suppose que vous voulez dire que F renvoie une classe conforme à X (et n'est pas une instance de X ) ? Par exemple:

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

Dans ce cas, les assertions doivent être :

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

Le Foo qui est dans la portée de ces instructions let a été muté par le décorateur. Le Foo d'origine n'est plus accessible. C'est effectivement équivalent à :

let Foo = F(class Foo {});

@nevir Oui, vous avez raison. Merci pour la clarification.

D'un autre côté, il semble que désactiver la vérification pour invalider les types de classe mutés est relativement facile :

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:
         }

Mais je ne suis pas assez informé pour que le compilateur produise les définitions de type correctes de la classe mutée. J'ai le test suivant :

tests/cas/conformité/décorateurs/classe/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();

Il génère des 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

je m'attendais à
>C: typeof C être >C: typeof X | typeof Y

Pour ceux qui sont intéressés par connect de react-redux comme étude de cas pour cette fonctionnalité, j'ai déposé https://github.com/DefinitelyTyped/DefinitelyTyped/issues/9951 pour suivre le problème en un seul endroit.

J'ai lu tous les commentaires sur ce problème et j'ai eu l'idée que la signature du décorateur ne montre pas réellement ce qu'il peut faire avec une classe enveloppée.

Considérez celui-ci :

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

Il doit être saisi de cette façon :
declare function decorator<T>(target: T): Wrapper<T>;

Mais cette signature ne nous dit pas que le décorateur a ajouté de nouvelles choses au prototype de la cible.

D'un autre côté, celui-ci ne nous dit pas que le décorateur a effectivement renvoyé un wrapper :
declare function decorator<T>(target: T): T & { someMethod: () => void };

des nouvelles à ce sujet? Ce serait super puissant pour la métaprogrammation !

Que diriez-vous d'une approche plus simple à ce problème? Pour une classe décorée, nous lions le nom de la classe à la valeur de retour du décorateur, comme un sucre syntaxique.

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.

Du point de vue de l'implémentation, cela introduira un symbole synthétique pour la classe décorée. Et le nom de la classe d'origine n'est lié qu'à la portée du corps de la classe.

@HerringtonDarkholme Je pense que ce serait une approche bien pragmatique qui fournirait la majeure partie de l'expressivité souhaitée. Bonne idée!

Je veux vraiment voir ça un jour

J'écris souvent une classe pour Angular 2 ou pour Aurelia, qui ressemble à ceci :

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

Ce que je veux écrire, c'est quelque chose comme
décorateurs/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;
    }
  }
}

et puis je pourrais l'appliquer de manière générique comme

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) { }
}

sans perte de sécurité de type. Ce serait une aubaine pour écrire du code propre et expressif.

Relancer ce problème.

Maintenant que # 13743 est sorti et que le support mixin est dans la langue, c'est une fonctionnalité super utile.

@HerringtonDarkholme est cependant moins adapté à ce cas, devoir déclarer le type de retour du décorateur perd certaines fonctionnalités dynamiques ...

@ahejlsberg , @mhegazy Pensez-vous que c'est faisable ?

J'ai un autre scénario d'utilisation dont je ne suis pas sûr qu'il soit encore couvert par cette conversation, mais qui relève probablement du même parapluie.

Je voudrais implémenter un décorateur de méthode qui change entièrement le type de la méthode (pas le type de retour ou les paramètres mais la fonction entière). par exemple

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

Totalement possible en JavaScript, ce n'est évidemment pas le problème, mais en TypeScript j'ai besoin du décorateur asyncTask pour changer le type de la méthode décorée de () => Promise<void> à AsyncTask<() => Promise<void>> .

Je suis sûr que ce n'est pas possible maintenant et relève probablement de ce problème ?

@codeandcats votre exemple est exactement le même cas d'utilisation pour lequel je suis ici !

Salut @ohjames , pardonnez-moi, j'ai du mal à comprendre votre exemple, avez-vous la possibilité de réécrire quelque chose qui fonctionne tel quel dans la cour de récréation ?

Des progrès à ce sujet? J'avais ça dans la tête toute la journée, ignorant ce problème, je suis allé l'implémenter seulement pour découvrir que le compilateur ne le détecte pas. J'ai un projet qui pourrait utiliser une meilleure solution de journalisation, j'ai donc écrit un singleton rapide pour le développer plus tard dans un enregistreur à part entière que j'allais attacher aux classes via un décorateur comme

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

et j'ai écrit le code nécessaire pour cela

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

et la propriété logger est définitivement présente à l'exécution mais malheureusement pas récupérée par le compilateur.

J'aimerais voir une solution à ce problème, d'autant plus qu'une construction d'exécution comme celle-ci devrait absolument pouvoir être correctement représentée au moment de la compilation.

J'ai fini par me contenter d'un décorateur immobilier juste pour me débrouiller pour l'instant :

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

et l'attacher à des classes comme

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

mais c'est beaucoup plus passe-partout par classe utilisant l'enregistreur qu'un simple décorateur de classe @loggable . Je suppose que je pourrais facilement transtyper comme (this as Loggable<this>).logger mais c'est aussi loin d'être idéal, surtout après l'avoir fait plusieurs fois. Ça deviendrait vite lassant.

Je devais TS pour une application entière principalement parce que je n'arrivais pas à faire fonctionner https://github.com/jeffijoe/mobx-task avec des décorateurs. J'espère que cela sera traité bientôt. 😄

C'est très irritant dans l'écosystème Angular 2 où les décorateurs et TypeScript sont traités comme des citoyens de première classe. Pourtant, à la minute où vous essayez d'ajouter une propriété avec un décorateur, le compilateur TypeScript dit non. J'aurais pensé que l'équipe Angular 2 s'intéresserait à ce problème.

@zajrik vous pouvez accomplir ce que vous voulez avec des mixins de classe qui ont été pris en charge avec un typage correct depuis TS 2.2 :

Définissez votre mixin Loggable comme ceci :

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

puis vous pouvez l'utiliser de plusieurs façons. Soit dans la clause extends d'une déclaration de classe :

class Foo {
  superProperty: string;
}

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

TS sait que les instances de LoggableFoo ont superProperty , logger et subProperty :

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

Vous pouvez également utiliser un mixin comme expression qui renvoie la classe concrète que vous souhaitez utiliser :

const LoggableFoo = Loggable(Foo);

Vous _pouvez_ également utiliser un mixin de classe comme décorateur, mais il a une sémantique légèrement différente, principalement sous-classe votre classe, plutôt que de permettre à votre classe de la sous-classer.

Les mixins de classe ont plusieurs avantages par rapport aux décorateurs, IMO :

  1. Ils créent une nouvelle superclasse, de sorte que la classe à laquelle vous les appliquez ait un changement pour les remplacer
  2. Ils tapent vérifier maintenant, sans aucune fonctionnalité supplémentaire de TypeScript
  3. Ils fonctionnent bien avec l'inférence de type - vous n'avez pas à taper la valeur de retour de la fonction mixin
  4. Ils fonctionnent bien avec l'analyse statique, en particulier le saut à la définition - Passer à l'implémentation de logger vous amène au mixin _implementation_, pas à l'interface.

@justinfagnani Je n'avais même pas pensé aux mixins pour ça alors merci. Je vais continuer et écrire un mixin Loggable ce soir pour rendre la syntaxe de ma pièce jointe Logger un peu plus agréable. La route extends Mixin(SuperClass) est ma préférée car c'est ainsi que j'ai utilisé les mixins jusqu'à présent depuis la sortie de TS 2.2.

Cependant, je préfère l'idée de la syntaxe du décorateur aux mixins, donc j'espère toujours qu'une solution pourra être trouvée pour ce problème spécifique. Être capable de créer des mixins sans passe-partout à l'aide de décorateurs serait une énorme aubaine pour un code plus propre, à mon avis.

@zajrik content que la suggestion ait aidé, j'espère

Je ne comprends toujours pas très bien comment les mixins ont plus de passe-partout que de décorateurs. Ils sont presque identiques en poids syntaxique :

Mixage de classe :

class LoggableFoo extends Loggable(Foo) {}

vs Décorateur :

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

À mon avis, le mixin est beaucoup plus clair sur son intention : il génère une superclasse, et les superclasses définissent les membres d'une classe, donc le mixin définit probablement également les membres.

Les décorateurs seront utilisés pour tant de choses que vous ne pouvez pas supposer qu'elles définissent ou non des membres. Il peut s'agir simplement d'enregistrer la classe pour quelque chose ou d'y associer des métadonnées.

Pour être juste, je pense que ce que @zajrik veut, c'est :

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

Ce qui est indéniablement, si jamais si légèrement, moins passe-partout.

Cela dit, j'adore la solution mixin. J'oublie toujours que les mixins sont une chose.

Si tout ce qui vous intéresse est d'ajouter des propriétés à la classe actuelle, les mixins sont fondamentalement équivalents aux décorateurs avec un inconvénient important... si votre classe n'a pas déjà de superclasse, vous devez créer une superclasse vide pour les utiliser. Aussi la syntaxe semble plus lourde en général. De plus, il n'est pas clair si les mixins paramétriques sont pris en charge (est extends Mixin(Class, { ... }) autorisé).

@justinfagnani dans votre liste de raisons, les points 2 à 4 sont en fait des lacunes dans TypeScript et non des avantages des mixins. Ils ne s'appliquent pas dans un monde JS.

Je pense que nous devrions tous être clairs sur le fait qu'une solution basée sur mixin au problème des OP impliquerait l'ajout de deux classes à la chaîne de prototypes, dont l'une est inutile. Cela reflète les différences sémantiques des décorateurs mixins Vs cependant, les mixins vous donnent une chance d'intercepter la chaîne de classe parent. Cependant, 95% du temps, ce n'est pas ce que les gens veulent faire, ils veulent décorer cette classe. Bien que les mixins aient leurs utilisations limitées, je pense que les promouvoir comme une alternative aux décorateurs et aux classes d'ordre supérieur est sémantiquement inapproprié.

Les mixins sont fondamentalement équivalents aux décorateurs avec un inconvénient important ... si votre classe n'a pas déjà de superclasse, vous devez créer une superclasse vide pour les utiliser

Ce n'est pas forcément vrai :

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

class Foo extends Mixin() {}

Aussi la syntaxe semble plus lourde en général.

Je ne vois tout simplement pas comment c'est ainsi, donc nous devrons être en désaccord.

De plus, il n'est pas clair si les mixins paramétriques sont pris en charge (étend Mixin(Class, { ... }) autorisé).

Ils le sont vraiment. Les mixins ne sont que des fonctions.

dans votre liste de raisons, les points 2 à 4 sont en fait des lacunes de TypeScript et non des avantages des mixins. Ils ne s'appliquent pas dans un monde JS.

Il s'agit d'un problème TypeScript, ils s'appliquent donc ici. Dans le monde JS, les décorateurs n'existent pas encore.

Je pense que nous devrions tous être clairs sur le fait qu'une solution basée sur mixin au problème des OP impliquerait l'ajout de deux classes à la chaîne de prototypes, dont l'une est inutile.

Je ne sais pas où vous obtenez deux. C'en est un, tout comme le décorateur pourrait le faire, à moins qu'il ne s'agisse d'un correctif. Et quel prototype est inutile ? L'application mixin ajoute vraisemblablement une propriété/méthode, ce n'est pas inutile.

Cela reflète les différences sémantiques des décorateurs mixins Vs cependant, les mixins vous donnent une chance d'intercepter la chaîne de classe parent. Cependant, 95% du temps, ce n'est pas ce que les gens veulent faire, ils veulent décorer cette classe.

Je ne suis pas sûr que ce soit vrai. Habituellement, lors de la définition d'une classe, vous vous attendez à ce qu'elle se trouve au bas de la hiérarchie d'héritage, avec la possibilité de remplacer les méthodes de la superclasse. Les décorateurs doivent soit corriger la classe, qui présente de nombreux problèmes, notamment ne pas fonctionner avec super() , soit l'étendre, auquel cas la classe décorée n'a pas la possibilité de remplacer l'extension. Cela peut être utile dans certains cas, comme un décorateur qui remplace chaque méthode définie de la classe pour le traçage des performances/débogage, mais c'est loin du modèle d'héritage habituel.

Bien que les mixins aient leurs utilisations limitées, je pense que les promouvoir comme une alternative aux décorateurs et aux classes d'ordre supérieur est sémantiquement inapproprié.

Lorsqu'un développeur souhaite ajouter des membres à la chaîne de prototypes, les mixins sont parfaitement sémantiquement appropriés. Dans tous les cas où j'ai vu quelqu'un vouloir utiliser des décorateurs pour les mixins, l'utilisation de mixins de classe accomplirait la même tâche, avec la sémantique qu'ils attendent réellement des décorateurs, plus de flexibilité grâce à la propriété de travail avec des super appels, et de Bien sûr, ils fonctionnent maintenant.

Les mixins ne sont guère inappropriés lorsqu'ils traitent directement du cas d'utilisation.

Lorsqu'un développeur souhaite ajouter des membres à la chaîne de prototypes

C'est exactement ce que je veux dire, l'OP ne veut rien ajouter à la chaîne de prototypes. Il veut juste muter une seule classe, et la plupart du temps, lorsque les gens utilisent des décorateurs, ils n'ont même pas de classe parent autre que Object. Et pour une raison quelconque, Mixin(Object) ne fonctionne pas dans TypeScript, vous devez donc ajouter une classe vide factice. Alors maintenant, vous avez une chaîne de prototypes de 2 (sans objet) lorsque vous n'en avez pas besoin. De plus, l'ajout de nouvelles classes à la chaîne de prototypes a un coût non négligeable.

Quant à la syntaxe, comparez Mixin1(Mixin2(Mixin3(Object, { ... }), {... }), {...}) . Les paramètres de chaque mixin sont aussi éloignés que possible de la classe mixin. La syntaxe du décorateur est clairement plus lisible.

Bien que la syntaxe du décorateur en soi ne vérifie pas le type, vous pouvez simplement utiliser l'invocation de fonction régulière pour obtenir ce que vous voulez :

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

C'est juste un peu ennuyeux que vous deviez déclarer votre classe comme const Foo = loggable(class { , mais à part ça, tout fonctionne.

@ohjames (cc @justinfagnani), vous devez être prudent lorsque vous étendez des commandes intégrées telles que Object (puisqu'elles écrasent le prototype de votre sous-classe dans les instances): https://github.com/Microsoft/TypeScript/wiki/FAQ # pourquoi-l'extension-des-incorporés-comme-le-tableau-et-la-carte-d'erreurs-ne-fonctionne-t-il pas ?

@nevir yep, j'ai déjà essayé la suggestion de @justinfagnani d'utiliser un mixin avec un paramètre par défaut Object dans le passé avec TypeScript 2.2 et tsc rejette le code.

@ohjames cela fonctionne toujours, il vous suffit de faire attention au prototype dans le cas par défaut (voir cette entrée de la FAQ).

Cependant, il est généralement plus facile de se fier au comportement de tslib.__extend lorsqu'il est passé null

Avez-vous l'intention de vous concentrer sur ce problème lors de la prochaine étape d'itération ? Les avantages de cette fonctionnalité sont extrêmement élevés dans de nombreuses bibliothèques.

Je viens de rencontrer ce problème - cela m'oblige à écrire beaucoup de code inutile. Résoudre ce problème serait d'une grande aide pour tout framework/bibliothèque basé sur un décorateur.

@TomMarius Comme je l'ai mentionné précédemment, les classes enveloppées dans des fonctions de décoration vérifient déjà correctement le type, vous ne pouvez tout simplement pas utiliser le sucre de syntaxe @ . Au lieu de faire :

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

il vous suffit de faire :

const Foo = loggable(class { });

Vous pouvez même composer un ensemble de fonctions de décorateur avant d'y intégrer une classe. Bien que le bon fonctionnement du sucre de syntaxe soit précieux, il ne semble pas que cela devrait être un problème aussi énorme que les choses.

@masaeedu Vraiment, le problème n'est pas le support de type externe mais interne. Pouvoir utiliser les propriétés que le décorateur ajoute dans la classe elle-même sans erreurs de compilation est le résultat souhaité, du moins pour moi. L'exemple que vous avez fourni ne donnerait que Foo le type loggable mais ne donnerait pas le type à la définition de classe elle-même.

@zajrik Un décorateur renvoie une nouvelle classe à partir d'une classe d'origine, même lorsque vous utilisez la syntaxe intégrée @ . De toute évidence, JS n'impose pas la pureté, vous êtes donc libre de muter la classe d'origine qui vous est transmise, mais cela est incompatible avec l'utilisation idiomatique du concept de décorateur. Si vous associez étroitement les fonctionnalités que vous ajoutez via des décorateurs aux composants internes de la classe, il peut également s'agir de propriétés internes.

Pouvez-vous me donner un exemple de cas d'utilisation d'une API de consommation interne de classe qui est ajoutée ultérieurement via des décorateurs ?

L'exemple Logger ci-dessus est un bon exemple d'un _want_ courant pour pouvoir manipuler les éléments internes de la classe décorée. (Et est familier aux personnes venant d'autres langages avec une décoration de classe, comme Python )

Cela dit, la suggestion de mixin de classe de @justinfagnani semble être une bonne alternative pour ce cas

Si vous voulez pouvoir définir les éléments internes d'une classe, la manière structurée de le faire n'est pas de patcher la classe ou de définir une nouvelle sous-classe, les deux sur lesquelles TypeScript aura du mal à raisonner dans le contexte de la classe lui-même, mais soit simplement définir les choses dans la classe elle-même, soit créer une nouvelle superclasse qui possède les propriétés nécessaires, sur lesquelles TypeScript peut raisonner.

Les décorateurs ne devraient vraiment pas modifier la forme d'une classe d'une manière visible pour la classe ou la plupart des consommateurs. @masaeedu est juste ici.

Bien que ce que vous dites soit vrai, TypeScript n'est pas là pour appliquer des pratiques de codage propres, mais pour taper correctement le code JavaScript, et cela échoue dans ce cas.

@masaeedu Ce que @zajrik a dit. J'ai un décorateur qui déclare un "service en ligne" qui ajoute un tas de propriétés à une classe qui sont ensuite utilisées dans la classe. Sous-classer ou implémenter une interface n'est pas une option en raison des métadonnées manquantes et de l'application des contraintes (si vous vous efforcez de ne pas dupliquer le code).

@TomMarius Mon point est qu'il vérifie correctement le type. Lorsque vous appliquez une fonction de décorateur à une classe, la classe n'est en aucun cas modifiée. Une nouvelle classe est produite via une transformation de la classe d'origine, et seule cette nouvelle classe est garantie de prendre en charge l'API introduite par la fonction de décorateur.

Je ne sais pas ce que signifie "métadonnées manquantes et application des contraintes" (peut-être qu'un exemple concret aiderait), mais si votre classe s'appuie explicitement sur l'API introduite par le décorateur, elle devrait simplement la sous-classer directement via le modèle mixin @justinfagnani montré , ou injectez-le via le constructeur ou quelque chose. L'utilité des décorateurs est qu'ils permettent aux classes fermées à la modification d'être étendues au profit du code qui consomme ces classes . Si vous êtes libre de définir la classe vous-même, utilisez simplement extends .

@masaeedu Si vous développez une sorte de, disons, une bibliothèque RPC, et que vous voulez forcer l'utilisateur à écrire uniquement des méthodes asynchrones, l'approche basée sur l'héritage vous oblige à dupliquer le code (ou je n'ai pas trouvé le bon moyen , peut-être - je serais heureux si vous me disiez si vous savez comment).

Approche basée sur l'héritage
Définition : export abstract class Service<T extends { [P in keyof T]: () => Promise<IResult>}> { protected someMethod(): Promise<void> { return Promise.reject(""); } }
Utilisation : export default class MyService extends Service<MyService> { async foo() { return this.someMethod(); } }

Approche basée sur le décorateur
Définition : export function service<T>(target: { new (): T & { [P in keyof T]: () => Promise<IResult> } }) { target.someMethod = function () { return Promise.reject(""); }; return target; }
Utilisation : <strong i="17">@service</strong> export default class { async foo() { return this.someMethod(); } }

Vous pouvez clairement voir la duplication de code dans l'exemple d'approche basée sur l'héritage. Il m'est arrivé plusieurs fois, à moi et à mes utilisateurs, d'avoir oublié de modifier le paramètre de type lorsqu'ils ont copié-collé la classe ou commencé à utiliser "any" comme paramètre de type et que la bibliothèque a cessé de fonctionner pour eux ; l'approche basée sur le décorateur est beaucoup plus conviviale pour les développeurs.

Après cela, il y a encore un autre problème avec l'approche basée sur l'héritage : les métadonnées de réflexion manquent maintenant, vous devez donc dupliquer encore plus le code car vous devez de toute façon introduire le décorateur service . L'utilisation est maintenant : <strong i="22">@service</strong> export default class MyService extends Service<MyService> { async foo() { return this.someMethod(); } } , et c'est tout simplement hostile, pas seulement un peu gênant.

Vous êtes vrai que sémantiquement, la modification est effectuée après la définition de la classe, cependant, il n'y a aucun moyen d'instancier la classe non décorée, il n'y a donc aucune raison de ne pas prendre en charge correctement la mutation de la classe autre que cela permet parfois du code impur (mais parfois pour le meilleur bien). N'oubliez pas que JavaScript est toujours basé sur des prototypes, la syntaxe de la classe n'est qu'un sucre pour le couvrir. Le prototype est modifiable et peut être désactivé par le décorateur, et il doit être correctement typé.

Lorsque vous appliquez une fonction de décorateur à une classe, la classe n'est en aucun cas modifiée.

Ce n'est pas vrai, lorsque vous appliquez une fonction de décorateur à une classe, la classe peut être modifiée, de quelque manière que ce soit. Que cela vous plaise ou non.

@TomMarius Vous essayez d'exploiter l'inférence pour faire respecter un contrat, ce qui n'a aucun rapport avec l'argument présenté ici. Tu devrais juste faire :

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

Il n'y a absolument aucune obligation pour les internes de la classe d'être conscients de la fonction de décoration.

@masaeedu Ce n'était pas mon propos. Le décorateur de service/classe Service introduit de nouvelles propriétés, et celles-ci sont toujours disponibles pour la classe à utiliser, mais le système de type ne reflète pas correctement cela. Vous avez dit que je devrais utiliser l'héritage pour cela, alors je vous ai montré pourquoi je ne peux pas/ne veux pas.

J'ai modifié l'exemple pour être plus clair.

@masaeedu Soit dit en passant, l'énoncé "Un décorateur renvoie une nouvelle classe à partir d'une classe d'origine" est incorrect - chaque décorateur que nous avons tous les deux montré ici renvoie une classe mutée ou directement la classe d'origine, jamais une nouvelle.

@TomMarius Votre commentaire mentionnait un problème avec "forcer l'utilisateur à écrire uniquement des méthodes asynchrones", qui est le problème que j'ai essayé de résoudre dans mon commentaire. Faire respecter le fait que l'utilisateur a suivi le contrat que vous attendez doit être fait partout où le code de l'utilisateur est renvoyé à la bibliothèque, et n'a rien à voir avec la discussion sur la question de savoir si les décorateurs doivent modifier la forme de type présentée aux internes de la classe. Le problème orthogonal de fournir une API au code de l'utilisateur peut être résolu avec des approches standard d'héritage ou de composition.

@ohjames La classe n'est pas modifiée simplement en appliquant un décorateur. JS n'impose pas la pureté, donc évidemment n'importe quelle déclaration n'importe où dans votre code peut modifier n'importe quoi d'autre, mais cela n'est pas pertinent pour cette discussion sur les fonctionnalités. Même une fois la fonctionnalité implémentée, TypeScript ne vous aidera pas à suivre les changements structurels arbitraires dans les corps de fonction.

@masaeedu Vous choisissez des morceaux, mais je parle de la situation dans son ensemble. Veuillez revoir tous mes commentaires dans ce fil - le point n'est pas dans les problèmes individuels mais dans chaque problème qui se produit en même temps. Je pense avoir bien expliqué le problème de l'approche basée sur l'héritage - beaucoup, beaucoup de duplication de code.

Pour plus de clarté, il ne s'agit pas de "code propre" pour moi. Le problème est d'ordre pratique; vous n'avez pas besoin de modifications massives du système de type si vous traitez @foo la même manière qu'une application de fonction. Si vous ouvrez la boîte de vers d'essayer d'introduire des informations de type dans l' argument d'une fonction à partir de son type de retour , tout en interagissant avec l'inférence de type et toutes les autres bêtes magiques trouvées dans divers coins du système de type TypeScript, je me sens cela deviendra un gros obstacle pour les nouvelles fonctionnalités de la même manière que la surcharge l'est maintenant.

@TomMarius Votre premier commentaire dans ce fil concerne le code propre, qui n'est pas pertinent. Le commentaire suivant concerne ce concept de service en ligne pour lequel vous avez fourni l'exemple de code. La principale plainte, du premier au quatrième paragraphe, concerne le fait que l'utilisation MyService extends Service<MyService> est sujette aux erreurs. J'ai essayé de montrer un exemple de la façon dont vous pouvez gérer cela.

Je l'ai revu et je ne vois vraiment rien dans cet exemple qui illustre pourquoi les membres de la classe décorée auraient besoin de connaître le décorateur. Qu'y a-t-il dans ces nouvelles propriétés que vous fournissez à l'utilisateur qui ne peuvent pas être accomplies avec l'héritage standard ? Je n'ai pas travaillé avec la réflexion, donc j'ai en quelque sorte survolé cela, mes excuses.

@masaeedu Je peux l'accomplir avec l'héritage, mais l'héritage m'oblige / mes utilisateurs à dupliquer massivement le code, donc j'aimerais avoir un autre moyen - et je le fais, mais le système de type n'est pas capable de refléter correctement la réalité.

Le fait est que le type correct de <strong i="7">@service</strong> class X { }service est déclaré comme <T>(target: T) => T & IService n'est pas X , mais X & IService ; et le problème est qu'en réalité c'est vrai même à l'intérieur de la classe - même si sémantiquement ce n'est pas vrai.

Un autre problème majeur causé par ce problème est que lorsque vous avez une série de décorateurs ayant chacun des contraintes, le système de type pense que la cible est toujours la classe d'origine, pas celle décorée, et donc la contrainte est inutile.

Je peux l'accomplir avec l'héritage, mais l'héritage me force/oblige mes utilisateurs à dupliquer massivement le code, donc j'aimerais avoir un autre moyen.

C'est la partie que je ne comprends pas. Vos utilisateurs doivent implémenter IService , et vous voulez vous assurer que TheirService implements IService obéit également à un autre contrat { [P in keyof blablablah] } , et peut-être voulez-vous aussi qu'ils aient un { potato: Potato } sur leur service. Tout cela est facile à accomplir sans que les membres de la classe soient conscients de @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 ""; }
}

Dans les deux cas, il n'y a pas de gain de géant dans la concision qui ne peut être obtenu qu'en présentant le type de retour de serviceDecorator comme le type de this à foo . Plus important encore, comment proposez-vous d'élaborer une stratégie de frappe uniforme pour cela ? Le type de retour de serviceDecorator est déduit en fonction du type de la classe que vous décorez, qui à son tour est maintenant typé comme le type de retour du décorateur ...

Salut @masaeedu , la concision devient particulièrement précieuse lorsque vous en avez plusieurs.

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

Cette solution de contournement n'est cependant qu'une alternative aux décorateurs de classe. Pour les décorateurs de méthodes, il n'y a actuellement aucune solution et bloque tellement de belles implémentations. Les décorateurs sont en proposition à l'étape 2 - https://github.com/tc39/proposal-decorators . Cependant, dans de nombreux cas, la mise en œuvre a été effectuée beaucoup plus tôt. Je pense que les décorateurs en particulier sont l'une de ces briques importantes qui étaient vraiment importantes car elles étaient déjà utilisées dans tant de frameworks et une version très simple est déjà implémentée dans babel/ts. Si ce problème pouvait être implémenté, il ne perdrait pas son état expérimental jusqu'à la sortie officielle. Mais c'est "expérimental" pour.

@pietschy Oui, faire fonctionner correctement le sucre de syntaxe @ avec la vérification de type serait bien. Pour le moment, vous pouvez utiliser la composition de fonctions pour obtenir une succinctitude raisonnablement similaire :

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

const MyComponent = decorator(class {
});

La discussion précédente est de savoir si c'est une bonne idée de faire une sorte de typage inverse où le type de retour du décorateur est présenté comme le type de this aux membres de la classe.

Salut @masaeedu , oui j'ai compris le contexte de la discussion, d'où le // do stuff with undo context, component methods etc . Acclamations

vraiment besoin de rendre Mixins plus facile.

typescript (javascript) ne prend pas en charge l'héritage multiple, nous devons donc utiliser Mixins ou Traits.
Et maintenant ça me fait perdre tellement de temps, surtout quand je reconstruis quelque chose.
Et je dois copier/coller une interface avec son "implémentation vide" partout. (#371)

--
Je ne pense pas que le type de retour du décorateur doive être présenté sur la classe.
parce que: .... euh. Je ne sais pas comment le décrire, désolé pour mes faibles compétences en anglais ... ( 🤔 une photo peut-elle exister sans cadre photo ? ) ce travail est pour un interface .

Je vais ajouter mon +1 à celui-ci ! J'aimerais le voir arriver bientôt.

@pietschy Si la classe dépend des membres ajoutés par les décorateurs, elle devrait alors étendre le résultat des décorateurs, et non l'inverse. Tu devrais faire:

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

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

L'alternative est que le système de type fonctionne dans une sorte de boucle, où l'argument de la fonction décoratrice est typé contextuellement par son type de retour, qui est déduit de son argument, qui est typé contextuellement par son type de retour, etc. Nous avons toujours besoin d'un proposition spécifique sur la façon dont cela devrait fonctionner, il ne s'agit pas simplement d'attendre la mise en œuvre.

Salut @masaeedu , je ne comprends pas pourquoi je devrais composer mes décorateurs et l'appliquer à une classe de base. Autant que je sache, le prototype a déjà été modifié au moment où tout code foncier utilisateur s'exécute. Par exemple

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

Donc, si les méthodes définies par les décorateurs existent déjà et peuvent légitimement être appelées, ce serait bien si le tapuscrit reflétait cela.

Le souhait est que le texte dactylographié reflète cela sans imposer de solutions de contournement. S'il y a d'autres
choses que les décorateurs peuvent faire qui ne peuvent pas être modélisées, alors ce serait au moins bien si cela
scénario était pris en charge sous une forme ou sous une autre.

Ce fil est plein d'opinions sur ce que les décorateurs devraient faire, comment ils devraient être utilisés, comment ils ne devraient pas être utilisés , etc.

Voici ce que font réellement les décorateurs :

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

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

qui devient, si vous ciblez esnext

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

avec __decorate appliquant chaque décorateur de bas en haut, chacun ayant la possibilité de renvoyer une toute nouvelle classe.

Je comprends que faire en sorte que le système de type prenne en charge et reconnaisse que cela peut très bien être délicat, et je comprends que cela peut très bien prendre du temps pour le prendre en charge, mais ne pouvons-nous pas tous convenir que le comportement actuel, à partir de l'exemple de code original ci-dessus qui a commencé ce fil, est tout simplement incorrect?

Quelle que soit l'opinion d'un développeur sur les décorateurs, les mixins ou la composition fonctionnelle, etc., etc., cela semble être un bogue assez clair.


Puis-je demander poliment si cela se trouve être dans le pipeline pour une future version ?

TypeScript est tout simplement incroyable et j'adore ça ; cela semble cependant être l'une des rares (seules ?) pièces qui est simplement cassée, et j'ai juste hâte de la voir réparée :)

@arackaf On comprend bien ce que font réellement les décorateurs, et en supposant que vous ne vous souciez pas des types émis par TypeScript, les décorateurs sont déjà pris en charge. Tout le débat dans ce numéro porte sur la manière dont les classes décorées doivent être présentées dans le système de types.

Je suis d'accord que new Foo().foo étant une erreur de type est un bogue, et facilement corrigé. Je ne suis pas d'accord pour dire que return this.foo; étant une erreur de type est un bogue. Si quoi que ce soit, vous demandez une fonctionnalité dans le système de type que jusqu'à présent, personne dans ce numéro n'a encore spécifié. Si vous avez en tête un mécanisme par lequel le type de this doit être transformé par un décorateur appliqué à la classe contenante, vous devez suggérer ce mécanisme explicitement .

Un tel mécanisme n'est pas aussi trivial qu'on pourrait s'y attendre, car dans presque tous les cas, le type de retour du décorateur est déduit du type même que vous proposez de transformer en utilisant le type de retour du décorateur. Si le décorateur addMethod prend une classe de type new () => T et produit new() => T & { hello(): void } , cela n'a aucun sens de suggérer que T devrait alors être T & { hello(): void } . Dans le corps du décorateur, est-il valide pour moi de faire référence à super.hello ?

Ceci est particulièrement pertinent car je ne suis pas limité à faire return class extends ClassIWasPassed { ... } dans le corps du décorateur, je peux faire ce que je veux ; les types de soustraction, les types mappés, les unions, ils sont tous équitables. Tout type résultant doit bien jouer avec cette boucle d'inférence que vous suggérez. Pour illustrer le problème, veuillez me dire ce qui devrait se passer dans cet exemple :

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"; }
}

Vous ne pouvez pas simplement écarter le problème comme étant "difficile", quelqu'un doit réellement proposer comment cela devrait fonctionner.

Ce n'est pas une erreur - vous transmettez à Logger une classe conforme à Greeter ; cependant Foo est ensuite muté par le décorateur pour être quelque chose de complètement différent, qui n'étend plus Greeter. Il devrait donc être valide pour le décorateur de l'enregistreur de traiter Foo comme Greeter, mais invalide pour quiconque, puisque Foo est réaffecté à quelque chose de complètement différent par le décorateur.

Je suis sûr que la mise en œuvre de cela serait extrêmement difficile. Un sous-ensemble raisonnable est-il possible, pour les cas courants comme celui répertorié en haut de ce fil, avec des solutions de contournement telles que la fusion d'interfaces comme solutions de repli pour les cas délicats comme celui-ci?

@arackaf

cependant Foo est ensuite muté par le décorateur pour être quelque chose de complètement différent

Il n'y a pas de définition précise de ce que Foo est en premier lieu. N'oubliez pas que notre point de discorde est de savoir si les membres Foo doivent pouvoir accéder (et donc revenir dans le cadre de l'API publique), les membres introduits par le décorateur à partir de this . Ce que Foo est est défini par ce que le décorateur renvoie, et ce que le décorateur renvoie est défini par ce que Foo est. Ils sont indissociables.

Je suis sûr que la mise en œuvre serait extrêmement difficile

Il est encore prématuré de parler de complexité de mise en œuvre, alors que nous n'avons même pas de proposition solide sur la manière dont la fonctionnalité devrait fonctionner. Toutes mes excuses pour avoir insisté là-dessus, mais nous avons vraiment besoin que quelqu'un propose un mécanisme concret pour "ce qui arrive à this quand j'applique une telle séquence de décorateurs à une telle classe". Nous pouvons ensuite brancher divers concepts TypeScript dans différentes parties et voir si ce qui en ressort a du sens. Ce n'est qu'alors qu'il est logique de discuter de la complexité de la mise en œuvre.

La sortie actuelle du décorateur que j'ai collée ci-dessus n'est-elle pas conforme aux spécifications ? J'ai supposé que c'était d'après ce que j'ai vu.

En supposant que ce soit le cas, Foo a une signification assez précise à chaque étape du processus. Je m'attendrais à ce que TS me permette d'utiliser this (méthodes Inside of Foo et sur des instances de Foo) en fonction de ce que le dernier décorateur renvoie, TS me forçant à ajouter des annotations de type en cours de route si nécessaire si le décorateur les fonctions ne sont pas suffisamment analysables.

@arackaf Il n'y a pas "d'étapes du chemin" ; nous ne nous intéressons qu'au type final de this pour un extrait de code immuable donné. Vous devez décrire, au moins à un niveau élevé, ce que vous attendez du type de this pour les membres d'une classe X décorée avec des fonctions de décorateur f1...fn , en termes des signatures de type de X et f1...fn . Vous pouvez avoir autant d'étapes dans le processus que vous le souhaitez. Jusqu'à présent, personne n'a fait cela. J'ai deviné que les gens veulent dire que le type de retour du décorateur devrait être présenté comme le type de this , mais pour autant que je sache, je pourrais être totalement à côté de la plaque.

Si votre proposition est de faire en sorte que les types reflètent mécaniquement ce qui arrive aux valeurs dans la sortie transpilée, vous vous retrouvez avec ce que je propose au lieu de ce que vous proposez : c'est-à-dire new Foo().hello() est bien, mais this.hello() n'est pas. À aucun moment dans cet exemple, la classe d'origine que vous décorez n'obtient une méthode hello . Seul le résultat de __decorate([addMethod], Foo) (qui est ensuite affecté à Foo ) a une méthode hello .

J'ai deviné que les gens veulent dire que le type de retour du décorateur devrait être présenté comme le type de ceci

Oh, désolé, oui, c'est vrai. Exactement ça. Arrêt complet. Parce que c'est ce que font les décorateurs. Si le dernier décorateur en ligne renvoie une classe complètement nouvelle, alors c'est ce qu'est Foo.

En d'autres termes:

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

Foo est tout ce que dans le monde c dit que c'est. Si c est une fonction qui renvoie any alors, je ne sais pas - peut-être juste revenir à l'original Foo ? Cela semble être une approche raisonnable et rétrocompatible.

mais si c renvoie un nouveau type X , alors je m'attendrais absolument à ce que Foo respecte cela.

Est-ce que je manque quelque chose?


clarifier davantage, si

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 

Est-ce que je manque quelque chose?

@arackaf Oui, ce qui manque à cette approche naïve, c'est qu'un décorateur est libre de renvoyer des types arbitraires, sans restriction que le résultat soit compatible avec le type de Foo .

Il y a un certain nombre d'absurdités que vous pouvez produire avec cela. Disons que je suspends mon objection à propos de la circularité de la saisie de this comme résultat de c , qui est déterminé par le type de this , qui est déterminé par le résultat de c , etc. Cela ne fonctionne toujours pas. Voici un autre exemple absurde :

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

Dans ce cas, vous êtes soit obligé de produire une erreur pour un code totalement bénin, soit d'ajuster la proposition selon laquelle this doit être exactement identique au type de retour du décorateur.

Je ne suis pas sûr de voir le problème. @logger a choisi de renvoyer une toute nouvelle classe, sans aucune méthode foo , et avec une toute nouvelle méthode hello() , qui fait référence à l'original, désormais inaccessible Foo .

new Foo().foo()

n'est plus valide ; cela produira une erreur d'exécution. Je dis juste que cela devrait également produire une erreur de compilation.

Cela dit, si l'analyse statique de tout cela est trop difficile, il serait tout à fait raisonnable de nous forcer à ajouter des annotations de type explicites à logger pour signifier exactement ce qui est renvoyé. Et si aucune annotation de type de ce type n'est présente, je dirais simplement de revenir à l'hypothèse que Foo est renvoyé. Cela devrait le garder rétrocompatible.

@arackaf Il n'y a aucun problème avec le code en termes de typage ou d'évaluation d'exécution. Je peux appeler new Foo().hello() , qui appellera en interne la classe décorée hello , qui appellera la classe décorée bar . Ce n'est pas une erreur pour moi d'invoquer bar dans la classe d'origine.

Vous pouvez l'essayer par vous-même en exécutant cet exemple complet dans la cour de récréation :

// Code from previous snippet...

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

Bien sûr - mais j'ai dit que c'était une erreur d'invoquer

new Foo().foo()

Il n'y a aucun problème avec le code en termes de typage ou d'évaluation d'exécution. Je peux appeler new Foo().hello(), qui appellera en interne le hello de la classe décorée, qui appellera le bar de la classe décorée

Mais ce devrait être une erreur de dire

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

puisque la méthode hello de Foo renvoie maintenant void, selon la classe renvoyée par Logger.

Bien sûr - mais j'ai dit que c'était une erreur d'invoquer new Foo().foo()

@arackaf Mais cela n'a pas d'importance. Je n'ai pas invoqué new Foo().foo() . J'ai invoqué this.foo() , et j'ai eu une erreur de type, même si mon code fonctionne bien à l'exécution.

Mais ce devrait être une erreur de dire let s : string = new Foo().hello()

Encore une fois, cela n'a aucune importance. Je ne dis pas que le type final de Foo.prototype.hello devrait être () => string (je suis d'accord qu'il devrait être () => void ). Je me plains de l'erreur d'invocation valide this.bar() , parce que vous avez chirurgicalement transplanté un type où il est absurde de le transplanter.

Il y a deux Foo ici. Quand tu dis

class Foo { 
}

Foo est une liaison immuable DANS la classe et une liaison mutable en dehors de la classe. Cela fonctionne donc parfaitement comme vous pouvez le vérifier dans un jsbin

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

let F = Foo;

Foo = null;

new F().hello();

Votre exemple ci-dessus fait des choses similaires ; il capture une référence à la classe d'origine avant que cette liaison externe ne soit mutée. Je ne sais toujours pas où vous voulez en venir.

this.foo(); est parfaitement valide, et je ne m'attendrais pas à une erreur de type (je ne blâmerais pas non plus les gens de TS si j'avais besoin d'une référence, car je suis sûr qu'il serait difficile de retracer cela)

this.foo(); est parfaitement valide, et je ne m'attendrais pas à une erreur de type

Bien, nous sommes donc d'accord, mais cela signifie que vous devez maintenant qualifier ou rejeter la proposition selon laquelle this soit le type d'instance de tout ce que le décorateur renvoie. Si vous pensez que cela ne devrait pas être une erreur de type, que devrait être this au lieu de { hello(): void } dans mon exemple ?

this dépend de ce qui a été instancié.

<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
}

Pourrions-nous nous en tenir à un exemple concret ? Cela me faciliterait grandement la compréhension. Dans l'extrait suivant :

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

Quel est le type de this ? Si c'est { hello(): void } , alors j'obtiendrai une erreur de type, car foo n'est pas membre de { hello(): void } . Si ce n'est pas { hello(): void } , alors this n'est pas simplement le type d'instance du type de retour du décorateur, et vous devez expliquer la logique alternative que vous utilisez pour arriver au type de this .

EDIT : J'ai oublié d'ajouter le décorateur sur Foo . Fixé.

this , où vous avez les flèches, est bien sûr une instance de l'original Foo . Il n'y a pas d'erreur de type.

Ah - je vois votre point maintenant; mais je ne vois toujours pas où est le problème. this.foo() WITHIN le Foo d'origine n'est pas une erreur de type - c'est valable pour la classe (maintenant inaccessible) qui était liée à l'identifiant Foo .

C'est une idiosyncrasie amusante, mais je ne vois pas pourquoi cela devrait empêcher TS de gérer les décorateurs de classe en mutation.

@arackaf Vous ne répondez pas à la question. Quel est précisément le type de this ? Vous ne pouvez pas simplement répondre en boucle sans fin " this est Foo et Foo est this ". Quels sont les membres this ? S'il a des membres en plus hello(): void , quelle logique est utilisée pour les déterminer ?

Lorsque vous dites " this.foo() DANS le Foo d'origine n'est pas une erreur de type", vous devez toujours répondre à la question : quel est le type structurel de this tel qu'il ne s'agisse pas d'une erreur de type à faire this.foo() ?

De plus, la classe d'origine n'est pas "inaccessible". Chaque fonction définie dans cet extrait de code est activement exercée au moment de l'exécution, et tout fonctionne sans accroc. Veuillez exécuter le lien du terrain de jeu que j'ai fourni et regarder la console. Le décorateur renvoie une nouvelle classe dans laquelle la méthode hello délègue à la méthode hello de la classe décorée, qui à son tour délègue à la méthode foo de la classe décorée.

C'est une idiosyncrasie amusante, mais je ne vois pas pourquoi cela devrait empêcher TS de gérer les décorateurs de classe en mutation.

Il n'y a pas de "trivia" dans le système de type. Vous n'obtiendrez pas une erreur de type TSC-1234 "méchant garçon, vous ne pouvez pas faire ça" car le cas d'utilisation est trop restreint. Si une fonctionnalité provoque des ruptures de code parfaitement normales de manière surprenante, la fonctionnalité doit être repensée.

Vous n'obtiendrez pas une erreur de type TSC-1234 "méchant garçon, vous ne pouvez pas faire ça" car le cas d'utilisation est trop restreint.

C'est EXACTEMENT ce que j'obtiens lorsque j'essaie d'utiliser une méthode qui a été ajoutée à une définition de classe par un décorateur. Je dois actuellement contourner ce problème en ajoutant une définition à la classe ou en utilisant la fusion d'interface, la conversion en any etc.

J'ai répondu à toutes les questions sur ce qu'est this et où.

Le simple fait est que la signification de this change en fonction de l'endroit où vous vous trouvez.

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"

quand vous dites new Class() - Class pointe vers le Foo d'origine, et du point de vue de TypeScript, il aurait accès à hello(): string puisque c'est ce que Class est tapé (étend Greeter). Au moment de l'exécution, vous instancierez une instance du Foo d'origine.

new LoggableFoo().hello() appelle une méthode void qui appelle une méthode autrement inaccessible, tapée via Greeter.

Si tu avais fait

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

alors Foo est maintenant une classe avec seulement hello() : void et new Foo().foo() devrait être une erreur de type.

Et, encore une fois, hello() { return this.foo(); } n'est pas une TypeError - pourquoi le serait-il ? Ce n'est pas parce que cette définition de classe n'est plus accessible que cette méthode est invalide.

Je ne m'attends pas à ce que TypeScript rende l'un de ces cas extrêmes parfait; il serait assez compréhensible d'avoir à ajouter des annotations de type ici et là, comme toujours. Mais aucun de ces exemples ne montre pourquoi @logger ne peut pas fondamentalement changer ce à quoi Foo est lié.

Si logger est une fonction qui renvoie une nouvelle classe, alors c'est ce à quoi Foo fait maintenant référence.

J'ai répondu à toutes les questions sur ce que c'est et où.
Le simple fait est que la signification de this change en fonction de l'endroit où vous vous trouvez.

C'est vraiment frustrant. Très bien, ça change, c'est Foo , c'est une liaison statique, etc. etc. etc. Quelle est la signature de type ? Quels sont les membres this ? Vous parlez de tout sous le soleil, alors que tout ce dont j'ai besoin de vous est une simple signature de type pour ce que this est à l'intérieur hello .

new LoggableFoo().hello() appelle une méthode void qui appelle une méthode autrement inaccessible, saisie via Greeter.

Ce n'est pas la même chose que inaccessible. Chaque méthode accessible est "inaccessible autrement" lorsque vous ne tenez pas compte des chemins par lesquels elle est accessible.

Si vous aviez fait :

Mais c'est littéralement ce que j'ai fait. C'est exactement mon extrait de code, collé à nouveau, précédé d'une explication de l'exemple que j'ai construit pour vous. Je l'ai même passé à travers un vérificateur de différences pour m'assurer que je ne prenais pas de pilules folles, et la seule différence est le commentaire que vous avez supprimé.

Et, encore une fois, hello() { return this.foo(); } n'est pas une TypeError - pourquoi le serait-il ?

Parce que vous (et d'autres ici) voulez que le type de this soit le type de retour instancié du décorateur, qui est { hello(): void } (notez l'absence d'un membre foo ). Si vous voulez que les membres de Foo puissent voir this comme type de retour du décorateur, le type de this à l'intérieur hello sera { hello(): void } . Si c'est { hello(): void } , j'obtiens une erreur de type. Si j'obtiens une erreur de type, je suis triste, car mon code fonctionne correctement.

Si vous dites que ce n'est pas une erreur de type, vous abandonnez votre propre schéma pour fournir le type de this via le type de retour du décorateur. Le type de this est alors { hello(): string; bar(): string } , quel que soit ce que le décorateur renvoie. Vous pouvez avoir un schéma alternatif pour produire le type de this qui évite ce problème, mais vous devez spécifier de quoi il s'agit.

Vous semblez ne pas comprendre qu'après l'exécution des décorateurs, Foo peut faire référence à quelque chose de totalement distinct de ce qu'il a été défini à l'origine.

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

Je suppose que vous trouvez une certaine étrangeté dans le fait this soit quelque chose de différent à l'intérieur de Foo ci-dessus, que dans les instances qui sont ensuite créées à partir de ce que Foo est finalement (après l'exécution des décorateurs) ?

Je ne sais pas vraiment quoi vous dire; c'est comme ça que fonctionnent les décorateurs. Mon seul argument est que TypeScript devrait correspondre plus étroitement à ce qui se passe.

Pour le dire autrement, les signatures de type à l'intérieur du Foo (original) seront différentes de ce que Foo est/produit une fois que les décorateurs ont exécuté.

Pour emprunter une analogie à un autre langage, à l'intérieur du Foo décoré, cela ferait référence à l'équivalent d'un type anonyme C# - un type totalement réel, qui est par ailleurs valide, mais qui ne peut pas vraiment être référencé directement. Cela aurait l'air bizarre d'obtenir les erreurs et les non-erreurs décrites ci-dessus, mais encore une fois, c'est comme ça que ça marche. Les décorateurs nous donnent un pouvoir énorme pour faire des choses bizarres comme celle-ci.

Je suppose que vous trouvez quelque chose d'étrange à ce qu'il s'agisse de quelque chose de différent à l'intérieur de Foo ci-dessus, que dans les instances qui sont ensuite créées à partir de ce que Foo est finalement (après l'exécution des décorateurs) ?

Non. Je ne trouve rien d'étrange à cela, car c'est exactement ce que je proposais il y a 200 commentaires. Avez-vous même lu l'une des discussions précédentes?

L'extrait que vous avez posté est totalement incontestable. Les personnes avec lesquelles j'étais en désaccord et dont vous avez sauté pour l'aide veulent également ce qui suit :

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

Je ne suis pas d'accord qu'il soit possible de le faire correctement et j'ai désespérément essayé de comprendre comment une telle suggestion fonctionnerait. Malgré tous mes efforts, je ne peux pas extraire une liste complète des membres this devrait avoir, ni comment cette liste serait construite dans le cadre de la proposition.

Pour le dire autrement, les signatures de type à l'intérieur du Foo (original) seront différentes de ce que Foo est/produit une fois que les décorateurs ont exécuté.

Alors pourquoi est-ce que tu te disputes avec moi ? J'ai dit: "Je suis d'accord que new Foo (). De manière analogue, dans votre exemple, je suis d'accord que new Foo().x() étant une erreur de type est un bogue, mais this.x() étant une erreur de type ne l'est pas.

Vous voyez comment il y a deux commentaires dans l'extrait en haut de cette page ?

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

^ Celui-là est celui que je trouve problématique. Soit vous convenez que le type de retour du décorateur ne doit pas être présenté sur this , et uniquement sur new Foo() , auquel cas nous nous disputons tous les deux pour rien. Ou vous n'êtes pas d'accord et souhaitez également cette fonctionnalité, auquel cas l'extrait de votre commentaire précédent n'est pas pertinent.

Je comprends enfin votre propos. Il m'a été extrêmement difficile d'obtenir cela à partir de votre code Greeter, mais je suis en train de suivre maintenant ; merci d'avoir été si patient.

Je dirais que la seule solution sensée serait que Foo ( à l'intérieur de Foo) prenne en charge l'union de type - derp je voulais dire intersection - du Foo d'origine, et tout ce que le dernier décorateur renvoie. Vous devez prendre en charge l'original Foo pour des exemples fous comme votre Greeter, et vous devez absolument prendre en charge tout ce que le dernier décorateur renvoie puisque c'est tout l'intérêt d'utiliser des décorateurs (selon les nombreux commentaires ci-dessus).

Alors oui, d'après mon exemple le plus récent, à l'intérieur de Foo x, y, z, a, b, c fonctionneraient tous. S'il y avait deux versions de a , alors prenez en charge les deux.

@arackaf np, merci également pour votre patience. Mes exemples n'ont pas été les plus clairs parce que je ne fais que publier tout ce que je peux concocter dans le terrain de jeu qui montre à quel point c'est cassé. Il m'est difficile d'y penser systématiquement.

Je dirais que la seule solution sensée serait que Foo (à l'intérieur de Foo) prenne en charge l'union de types du Foo d'origine, et tout ce que le dernier décorateur renvoie.

Ok génial, donc nous entrons dans les détails maintenant. Corrigez-moi si je me trompe, mais quand vous dites "union", vous voulez dire qu'il devrait avoir les membres de type des deux, c'est-à-dire qu'il devrait être A & B . Nous voulons donc que this soit typeof(new OriginalClass()) & typeof(new (decorators(OriginalClass))) , où decorators est la signature de type composée de tous les décorateurs . En clair, nous voulons que this soit l'intersection du type instancié de la "classe d'origine" et du type instancié de la "classe d'origine" passé par tous les décorateurs.

Il y a deux problèmes avec cela. La première est que dans des cas comme mon exemple, cela vous permet simplement d'accéder à des membres inexistants. Je pourrais ajouter un groupe de membres dans le décorateur, mais si j'essayais d'y accéder dans ma classe en utilisant this.newMethod() , cela ne ferait que vomir au moment de l'exécution. newMethod n'est ajouté qu'à la classe renvoyée par la fonction de décorateur, les membres de la classe d'origine n'y ont pas accès (à moins que j'utilise spécifiquement le modèle return class extends OriginalClass { newMethod() { } } ).

L'autre problème est que la "classe d'origine" n'est pas un concept bien défini. Si je peux accéder aux membres décorés à partir de this , je peux également les utiliser dans le cadre des instructions de retour, et donc ils peuvent faire partie de l'API publique de la "classe d'origine". Je fais un peu signe de la main ici, et je suis un peu trop fatigué pour penser à des exemples concrets, mais je pense que si nous y réfléchissions sérieusement, nous pourrions trouver des exemples absurdes. Peut-être pourriez-vous contourner ce problème en trouvant un moyen de séparer les membres qui ne renvoient pas les éléments auxquels ils ont accédé à partir de this ou du moins pour lesquels le type de retour n'est pas déduit à la suite du retour de this.something() .

@masaeedu ouais, je me suis corrigé sur le truc union / intersection avant votre réponse. C'est contre-intuitif pour quelqu'un qui découvre TS.

Compris sur le reste. Honnêtement, les décorateurs ne renverront généralement pas un type complètement différent, ils augmenteront généralement simplement le type qui a été transmis, de sorte que l'intersection "fonctionnera" en toute sécurité la plupart du temps.

Je dirais que les erreurs d'exécution dont vous parlez seraient rares et le résultat de certaines décisions de développement délibérément mauvaises. Je ne suis pas sûr que vous devriez vraiment vous en soucier mais, si c'est vraiment un problème, je dirais que le simple fait d'utiliser ce que le dernier décorateur a renvoyé serait une deuxième place décente (alors oui, une classe pourrait voir une TypeError par essayer d'utiliser une méthode qui se définit elle-même - pas idéale, mais toujours un prix intéressant à payer pour les décorateurs travaillant).

Mais vraiment, je pense que les erreurs d'exécution auxquelles vous pensez ne valent pas la peine d'être évitées, certainement au détriment du bon fonctionnement des décorateurs. De plus, il est assez facile de produire des erreurs d'exécution dans TS si vous êtes négligent ou stupide.

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

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

Concernant votre seconde objection

Je peux également les utiliser dans le cadre des instructions de retour, et par conséquent, ils peuvent faire partie de l'API publique de la "classe d'origine"

J'ai bien peur de ne pas y voir de problèmes. Je veux que tout ce qui est ajouté à la classe via un décorateur soit absolument un citoyen de première classe. Je suis curieux de savoir quels seraient les problèmes potentiels.

Je pense qu'une autre bonne option serait de ne l'implémenter que partiellement.

Actuellement, les classes sont toujours typées telles qu'elles sont définies. Donc, à l'intérieur de Foo, cela est basé sur la définition de foo, quels que soient les décorateurs. Ce serait une énorme, énorme amélioration de "juste" étendre cela pour un sous-ensemble utile de cas d'utilisation de décorateurs (c'est-à-dire les plus courants)

Et si vous autorisez l'extension de la classe (du point de vue de this à l'intérieur de la classe) si et seulement si le décorateur renvoie quelque chose qui étend l'original, c'est-à-dire pour

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

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

Faites-le fonctionner et fournissez un support de première classe à blah et à tout ce qui est ajouté par le décorateur. Et pour les cas d'utilisation qui font des choses folles comme renvoyer une toute nouvelle classe (comme votre Greeter), continuez simplement le comportement actuel et ignorez ce que fait le décorateur.


Au fait, peu importe ce que vous choisissez, comment pourrais-je annoter cela ? Cela peut-il être annoté actuellement ? J'ai essayé

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

ainsi que de nombreuses variations sur ce thème, mais TypeScript n'en avait aucune :)

@arackaf Un décorateur n'est qu'une fonction. Dans le cas général, vous l'obtiendrez à partir d'un fichier .d.ts quelque part et vous n'aurez aucune idée de la façon dont il est implémenté. Vous ne savez pas si l'implémentation renvoie une toute nouvelle classe, en ajoutant/soustrayant/remplaçant des membres sur le prototype de la classe d'origine et en la renvoyant, ou en étendant la classe d'origine. Tout ce dont vous disposez est le type de retour structurel de la fonction.

Si vous voulez lier d'une manière ou d'une autre les décorateurs à l'héritage de classe, vous devez d'abord proposer un concept de langage distinct pour JS. La façon dont les décorateurs fonctionnent aujourd'hui ne justifie pas la mutation this dans le cas général. Par exemple, personnellement, je préfère toujours la composition à l'héritage, et je ferais toujours :

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

Ce n'est pas une expérience académique folle, c'est une approche standard pour faire des mixins, et ça rompt avec l'exemple que je vous ai donné. En fait, presque tout sauf return class extends Class rompt avec l'exemple que je vous ai donné, et dans de nombreux cas, les choses atteindront le seuil de rentabilité avec return class extends Class .

Vous allez devoir sauter à travers toutes sortes de cerceaux et de contorsions pour que cela fonctionne, et le système de type vous combattra à chaque étape du processus, car ce que vous faites est absurde dans le cas général. Plus important encore, une fois que vous l'aurez implémenté, tout le monde devra manœuvrer de manière acrobatique autour de ce coin absurde du système de type chaque fois qu'il essaiera d'implémenter un autre concept complexe (mais sain).

Ce n'est pas parce que vous avez ce cas d'utilisation que vous jugez important (et j'ai essayé plusieurs fois dans ce fil de démontrer d'autres moyens d'exprimer ce que vous voulez dans le système de type existant), cela ne signifie pas que la seule bonne chose faire est de gâcher à tout prix votre approche suggérée. Vous pouvez fusionner des interfaces arbitraires avec votre classe, y compris les types de retour des fonctions de décorateur, ce n'est donc pas comme s'il vous était impossible d'aller n'importe où si vous insistez pour utiliser les décorateurs comme vous les utilisez.

@arackaf ceci :

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

Devrait fonctionner correctement dans la clause extend :

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

Avec une sémantique beaucoup plus facile et déjà définie dans le système de type. Cela fonctionne déjà. Quel problème essayez-vous de résoudre en sous-classes la classe déclarée, plutôt que de créer une super-classe pour elle ?

@masaeedu

La façon dont les décorateurs travaillent aujourd'hui ne justifie pas de muter cela dans le cas général.

L'utilisation principale des décorateurs (de classe) est absolument, positivement, de muter la valeur de this à l'intérieur de la classe, d'une manière ou d'une autre. @connect @observer MobX prennent tous deux une classe et crachent une NOUVELLE version de la classe originale, avec des fonctionnalités supplémentaires. Dans ces cas particuliers, je ne pense pas que la structure réelle de this change (juste la structure de this.props ), mais cela n'a pas d'importance.

C'est un cas d'utilisation assez courant (comme vous pouvez le voir dans les commentaires ci-dessus) pour utiliser un décorateur pour muter une classe d'une manière ou d'une autre). Pour le meilleur ou pour le pire, les gens ont tendance à ne pas aimer les alternatives syntaxiques

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

par opposition à

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

Maintenant, qu'un décorateur fasse

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

ou

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

ne devrait pas avoir d'importance. D'une manière ou d'une autre, le cas d'utilisation que beaucoup réclament vraiment, est de pouvoir dire à TypeScript, par toutes les annotations nécessaires, peu importe à quel point cela nous dérange, que function c prend une classe C in, et retourne une classe qui a la structure C & {blah(): void} .

C'est pourquoi beaucoup d'entre nous utilisent activement les décorateurs aujourd'hui. Nous voulons vraiment, vraiment intégrer un sous-ensemble utile de ce comportement dans le système de types.

Il est facile de démontrer que les décorateurs peuvent faire des choses bizarres qui seraient impossibles à suivre pour le système de type. Amende! Mais il doit y avoir un moyen d'annoter cela

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

est valable. Je ne sais pas si cela nécessitera une nouvelle syntaxe d'annotation de type pour c , ou si la syntaxe d'annotation de type existante pourrait être mise en service sur c pour accomplir cela, mais juste en corrigeant l'utilisation générale cas (et laisser les bords tels quels, sans effet sur la classe d'origine) serait une énorme aubaine pour les utilisateurs de TS.

@justinfagnani pour info ceci

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

produit

Le type 'typeof (classe anonyme)' n'est pas assignable au type 'T & (new () => { blah(): void; })'.
Le type 'typeof (classe anonyme)' n'est pas assignable au type 'T'.

dans l'aire de jeux TS. Je ne sais pas si certaines options sont mal définies.

Quel problème essayez-vous de résoudre en sous-classes la classe déclarée, plutôt que de créer une super-classe pour elle ?

J'essaie simplement d'utiliser des décorateurs. Je les utilise avec plaisir dans JS depuis plus d'un an - j'ai juste hâte que TS me rattrape. Et je ne suis certainement pas marié au sous-classement. Parfois, le décorateur modifie simplement le prototype de la classe d'origine pour ajouter des propriétés ou des méthodes. Dans tous les cas, le but est de dire à TypeScript que

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

se traduira par une classe avec de nouveaux membres, créée par c .

@arackaf Nous sommes à nouveau désynchronisés. Le débat ne porte pas là-dessus :

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

contre ceci :

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

Je m'attends à ce que vous puissiez utiliser le premier et que vous ayez toujours la bonne frappe de new Foo() . Le débat porte sur ceci :

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

contre ceci :

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

Ce dernier fonctionnera sans casser le système de type. Le premier est problématique à plusieurs égards que j'ai déjà illustrés.

@masaeedu n'y a-t-il vraiment pas de cas d'utilisation restreints dans lesquels le premier pourrait fonctionner ?

Pour être précis: n'y a-t-il pas d'annotations que TypeScript pourrait nous dire d'ajouter à a , b et c de votre exemple ci-dessus afin que le système de type traite l'ancien indistinctement de ce dernier ?

Ce serait une avancée majeure pour TypeScript.

@arackaf désolé, vous devez faire en sorte que le paramètre de type se réfère au type de constructeur, pas au type d'instance :

Cela marche:

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

J'essaie simplement d'utiliser des décorateurs.

Ce n'est pas vraiment un problème que vous essayez de résoudre, et c'est une tautologie. C'est comme Q: "qu'essayez-vous de faire avec ce marteau?" A: "Utilisez simplement le marteau".

Avez-vous un cas réel à l'esprit où la génération d'une nouvelle superclasse avec l'extension prévue ne fonctionne pas ? Parce que TypeScript prend en charge cela maintenant, et comme je l'ai dit, il est beaucoup plus facile de raisonner sur le type de la classe depuis l'intérieur de la classe dans ce cas.

J'ai regardé l'annotation @observer Mobx et il semble qu'elle ne change pas la forme de la classe (et pourrait facilement être un décorateur de méthode plutôt qu'un décorateur de classe puisqu'elle n'enveloppe que render() ).

@connect est en fait un excellent exemple de la complexité de taper correctement les décorateurs, car @connect semble même ne pas renvoyer une sous-classe de la classe décorée, mais une toute nouvelle classe qui enveloppe la classe décorée, mais les statiques sont copiées, de sorte que le résultat supprime l'interface côté instance et préserve le côté statique que TS ne vérifie même pas dans la plupart des cas. Il semble que @connect ne devrait pas du tout être un décorateur et connect devrait simplement être utilisé comme HOC, car en tant que décorateur, c'est terriblement déroutant.

Je les utilise avec plaisir dans JS depuis plus d'un an

Eh bien... vous ne l'avez pas vraiment fait. JS n'a pas de décorateurs. Vous avez probablement utilisé une proposition précédente obsolète particulière, qui se trouve être implémentée légèrement différemment dans Babel et TypeScript. Dans d'autres lieux, je plaide pour que les décorateurs poursuivent le processus de normalisation, mais les progrès ont ralenti et je ne pense pas que ce soit encore une certitude. Beaucoup soutiennent qu'ils ne devraient pas être ajoutés, ou seulement des annotations, de sorte que la sémantique peut même encore être en suspens.

L'une des plaintes contre les décorateurs est exactement qu'ils peuvent changer la forme de la classe, ce qui rend difficile le raisonnement statique, et certaines propositions ont été évoquées au moins pour limiter la capacité des décorateurs à le faire, même si je pense que c'était avant le la proposition la plus récente a un langage de spécification. Je soutiens personnellement que les décorateurs bien élevés ne devraient pas changer la forme de la classe, de sorte qu'aux fins de l'analyse statique, ils puissent être ignorés. N'oubliez pas que le JS standard n'a pas de système de type sur lequel s'appuyer pour indiquer à l'analyse ce que fait une implémentation de fonction.

  • J'ai juste hâte que TS me rattrape.

Je ne vois pas en quoi TypeScript est en retard ici. Au moins, derrière quoi ? Les décorateurs travaillent. Existe-t-il un autre JS typé où les classes se comportent comme vous le souhaitez ?

Les décorateurs sont maintenant à l'étape 2 et sont utilisés dans à peu près tous les frameworks JS.

Quant à @connect , react-redux compte environ 2,5 millions de téléchargements par mois ; il est incroyablement arrogant d'entendre que la conception est en quelque sorte "mauvaise" parce que vous l'auriez implémentée différemment.

Les tentatives actuelles d'utilisation de méthodes introduites par un décorateur entraînent des erreurs de type dans TypeScript, nécessitant des solutions de contournement telles que la fusion d'interfaces, l'ajout manuel de l'annotation pour les méthodes ajoutées ou tout simplement le fait de ne pas utiliser de décorateurs pour faire muter des classes (comme si quelqu'un avait besoin d'être informé que nous pouvions simplement pas utiliser de décorateurs).

N'y a-t-il vraiment aucun moyen d'informer manuellement et minutieusement le compilateur TS qu'un décorateur modifie la forme d'une classe ? Peut-être que c'est fou mais TS ne pouvait pas simplement expédier un nouveau décorateurtype que nous pourrions utiliser

soit c : décorateur

Et SI (et seulement si) des décorateurs de ce type sont appliqués à une classe, TS considérera que la classe est de type Input & { blah() } ?

Littéralement, tout le monde sait que nous ne pouvons tout simplement pas utiliser de décorateurs. La plupart savent aussi qu'un groupe vocal n'aime pas les décorateurs. Cet article explique comment TS pourrait éventuellement, d'une manière ou d'une autre, faire comprendre à son système de type qu'un décorateur est en train de muter une définition de classe. Ce serait une aubaine pour beaucoup de gens.

Pour être précis : n'y a-t-il pas d'annotations que TypeScript pourrait nous dire d'ajouter à a, b et c de votre exemple ci-dessus afin que le système de type traite le premier de manière indiscernable du second ?

Oui, il est très facile de déclarer la forme que vous voulez pour Foo , il vous suffit d'utiliser la fusion d'interface. Il vous suffit de faire :

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

À l'heure actuelle, vous devez probablement vous attendre à ce que la bibliothèque de décorateurs que vous utilisez exporte les types paramétrés correspondant à ce qu'ils renvoient, ou vous devrez déclarer vous-même les membres qu'ils ajoutent. Si nous obtenons # 6606, vous pouvez simplement faire interface Foo extends typeof(a(b(c(Foo)))) .

Les tentatives actuelles d'utilisation de méthodes introduites par un décorateur entraînent des erreurs de type dans TypeScript, nécessitant des solutions de contournement telles que la fusion d'interfaces, l'ajout manuel de l'annotation pour les méthodes ajoutées ou tout simplement le fait de ne pas utiliser de décorateurs pour faire muter des classes (comme si quelqu'un avait besoin d'être informé que nous pouvions simplement pas utiliser de décorateurs).

Vous n'avez pas mentionné la possibilité de décorer la classe que vous écrivez lorsqu'elle est indépendante des membres présentés par le décorateur et de la faire étendre une classe décorée lorsqu'elle ne l'est pas. C'est ainsi que j'utiliserai au moins les décorateurs.

Pourriez-vous me donner un extrait de code d'une bibliothèque que vous utilisez en ce moment où c'est un problème ?

Désolé - je n'ai pas suivi votre dernier commentaire, mais votre commentaire précédent, avec la fusion de l'interface, est exactement la façon dont je travaille autour de cela.

Actuellement, mon décorateur doit également exporter un Type, et j'utilise la fusion d'interface exactement comme vous l'avez montré, pour indiquer à TS que de nouveaux membres sont ajoutés par le décorateur. C'est la capacité d'abandonner ce passe-partout supplémentaire que beaucoup recherchent tant.

@arackaf Peut-être que ce que vous voulez vraiment, c'est un moyen pour la fusion des déclarations d'interagir avec le typage contextuel des arguments de la fonction.

Sûr. Je veux avoir la possibilité d'utiliser un décorateur sur une classe et, conceptuellement, en fonction de la façon dont j'ai défini le décorateur, d'avoir une fusion d'interface, ce qui entraînera l'ajout de membres supplémentaires à ma classe décorée.

Je n'ai aucune idée de comment je procéderais pour annoter mon décorateur pour que cela se produise, mais je ne suis pas vraiment pointilleux - toute syntaxe qu'une future version de TS me donnerait pour affecter me rendrait incroyablement heureux :)

Les décorateurs sont maintenant à l'étape 2 et sont utilisés dans à peu près tous les frameworks JS.

Et ils n'en sont qu'au stade 2 et se déplacent extrêmement lentement. J'ai très envie que les décorateurs arrivent, et j'essaie de comprendre comment je peux les aider à avancer, mais ce n'est toujours pas une certitude. Je connais encore des personnes susceptibles d'avoir une influence sur le processus qui s'y opposent ou souhaitent qu'elles soient limitées à des annotations, même si je ne pense pas qu'elles interféreront. Mon point sur les implémentations actuelles du décorateur de compilateur n'étant pas JS est valable.

Quant à @connect , react-redux compte environ 2,5 millions de téléchargements par mois ; il est incroyablement arrogant d'entendre que la conception est en quelque sorte "mauvaise" parce que vous l'auriez implémentée différemment.

Veuillez vous abstenir d'attaques personnelles comme me traiter d'arrogant.

  1. Je ne t'ai pas attaqué personnellement, alors ne m'attaque pas personnellement.
  2. Cela n'aide pas du tout votre argumentation.
  3. La popularité d'un projet ne doit pas l'exclure de la critique technique.
  4. Parlons-nous de la même chose ? redux-connect-decorator a 4 téléchargements par jour. La fonction connect() de react-redux ressemble à un HOC, comme je l'ai suggéré.
  5. Je pense que mon opinion selon laquelle les décorateurs bien élevés ne devraient pas changer la forme publique d'une classe, et devraient certainement la remplacer par une classe non liée, est assez raisonnable et trouverait un accord suffisant autour de la communauté JS. C'est assez loin d'être arrogant, même si vous n'êtes pas d'accord.

Les tentatives actuelles d'utilisation de méthodes introduites par un décorateur entraînent des erreurs de type dans TypeScript, nécessitant des solutions de contournement telles que la fusion d'interfaces, l'ajout manuel de l'annotation pour les méthodes ajoutées ou tout simplement le fait de ne pas utiliser de décorateurs pour faire muter des classes (comme si quelqu'un avait besoin d'être informé que nous pouvions simplement pas utiliser de décorateurs).

Ce n'est pas que nous suggérons simplement de ne pas utiliser de décorateurs du tout, c'est que je crois que @masaeedu dit que dans le but de modifier un prototype de manière sécurisée, nous avons des mécanismes existants : héritage et application mixin. J'essayais de demander pourquoi votre exemple d ne convenait pas pour être utilisé comme mixin, et vous n'avez pas donné de réponse, sauf pour dire que vous vouliez simplement utiliser des décorateurs.

C'est bien, je suppose, mais cela semble limiter la conversation de manière assez critique, donc je vais me retirer à nouveau.

@justinfagnani dont je parlais

import {connect} from 'react-redux'

connect il y a juste une fonction qui prend une classe (composant) et en crache une autre, comme vous l'avez dit ci-dessus.

Actuellement, dans Babel et TypeScript, j'ai la possibilité d'utiliser connect comme ceci

class UnConnectedComponent extends Component {
}

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

OU comme ça

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

Il se trouve que je préfère ce dernier, mais si vous ou quelqu'un d'autre préférez le premier, alors qu'il en soit ainsi; de nombreuses routes mènent à Rome. je préfère pareillement

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

sur

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

ou

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

Je n'ai pas répondu explicitement à votre question uniquement parce que je pensais que la raison pour laquelle j'utilisais des décorateurs était claire, mais je vais l'énoncer explicitement: je préfère l'ergonomie et la syntaxe fournies par les décorateurs. Tout ce fil de discussion consiste à essayer d'obtenir une certaine capacité à dire à TS comment nos décorateurs font muter la classe en question afin que nous n'ayons pas besoin d'un passe-partout comme la fusion d'interfaces.

Comme OP l'a dit il y a bien longtemps, nous voulons juste

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'

juste travailler. Le fait qu'il existe d'autres alternatives à cette syntaxe spécifique est à la fois évident et sans importance. Le fait que de nombreux membres de la communauté JS/TS n'aiment pas cette syntaxe particulière est également évident et sans importance.

Si TS pouvait nous donner un moyen, même restreint, de faire fonctionner cette syntaxe, ce serait un énorme avantage pour le langage et la communauté.

@arackaf Vous avez oublié de mentionner comment l'utilisation connect nécessite l'accès à des membres supplémentaires sur this . AFAICT, votre implémentation de composant est censée être totalement indépendante de ce que fait connect , elle n'utilise que ses propres props et state . Donc, dans ce sens, la façon dont connect est conçu correspond plus à l'utilisation des décorateurs par @justinfagnani qu'à la vôtre.

Pas vraiment. Envisager

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

puis plus tard

<Foo />

cela devrait être valide - les accessoires PropsShape proviennent du magasin Redux, mais TypeScript ne le sait pas, car il s'écarte de la définition originale de Foo, donc je finis par obtenir des erreurs pour les accessoires manquants et je dois le convertir en any .

Mais pour être clair, le cas d'utilisation précis donné à l'origine par OP, et dupliqué dans mon deuxième commentaire ci-dessus ici, est extrêmement courant dans notre base de code, et à première vue, dans beaucoup d'autres également. Nous utilisons littéralement des choses comme

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

et doivent ajouter une fusion d'interface afin de satisfaire TypeScript. Ce serait merveilleux d'avoir un moyen d'intégrer cette interface de manière transparente dans la définition du décorateur.

On continue de tourner en rond avec ça. Nous convenons que le type de Foo devrait être le type de retour de connect . Nous sommes tout à fait d'accord là-dessus.

Là où nous ne sommes pas d'accord, c'est si les membres à l'intérieur Foo doivent prétendre que this est une sorte de fusion récursive du type de retour du décorateur et du "Foo original", quoi que cela signifie. Vous n'avez pas démontré de cas d'utilisation pour cela.

Là où nous ne sommes pas d'accord, c'est si les membres à l'intérieur de Foo doivent prétendre qu'il s'agit d'une sorte de fusion récursive du type de retour du décorateur et du "Foo original", quoi que cela signifie. Vous n'avez pas démontré de cas d'utilisation pour cela.

J'ai. Voir mon commentaire ci-dessus, et d'ailleurs le code original d'OP. Il s'agit d'un cas d'utilisation extrêmement courant.

Veuillez me montrer ce que connect ajoute au prototype auquel il s'attend à ce que vous accédiez à l'intérieur du composant. Si la réponse est « rien », alors s'il vous plaît, n'affichez pas à nouveau connect , car cela n'a aucun intérêt.

@masaeedu assez juste, et je suis d'accord. Je répondais principalement aux affirmations de @justinfagnani sur la façon dont les décorateurs ne devraient pas modifier la classe d'origine, connect a été mal conçu, etc etc etc.

Je démontrais simplement la syntaxe que moi et beaucoup d'autres préférons, malgré le fait que d'autres options existent.

Pour ce cas, oui, je ne connais pas de bibliothèques npm populaires qui prennent une classe et recrachent une nouvelle classe avec de nouvelles méthodes, qui seraient utilisées à l'intérieur de la classe décorée. C'est quelque chose que moi et d'autres faisons fréquemment, dans notre propre code, pour implémenter nos propres mixins, mais je n'ai pas vraiment d'exemple sur npm.

Mais il n'est pas vraiment contesté qu'il s'agit d'un cas d'utilisation courant, n'est-ce pas ?

@arackaf Non, ce n'est pas le cas car vous n'accédez à aucun membre sur this qui est introduit par @connect . En fait, je suis à peu près sûr de cet extrait précis :

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

compilera sans aucun problème dans TypeScript aujourd'hui. L'extrait que vous avez collé n'a rien à voir avec la fonctionnalité que vous demandez.

@masaeedu - désolé - j'ai réalisé que j'avais tort et j'ai supprimé ce commentaire. Pas assez rapide pour vous éviter de perdre votre temps cependant :(

@arackaf Je ne sais pas à quel point un modèle est courant, et personnellement, je ne l'ai ni utilisé moi-même ni utilisé aucune bibliothèque qui l'utilise. J'ai utilisé Angular 2 pendant un certain temps, donc ce n'est pas comme si je n'avais jamais utilisé de décorateurs de ma vie. C'est pourquoi je demandais des exemples spécifiques d'utilisation d'une bibliothèque où l'impossibilité d'accéder aux membres présentés par le décorateur dans le décorer est un point douloureux.

Dans tous les cas où les membres d'une classe attendent quelque chose de this , il devrait y avoir quelque chose dans le corps de la classe ou dans la clause extends de la classe qui satisfait à l'exigence. C'est comme ça que ça a toujours fonctionné, même avec les décorateurs . Si vous avez un décorateur qui ajoute des fonctionnalités dont dépend une classe, la classe doit étendre tout ce que le décorateur renvoie, et non l'inverse. C'est juste du bon sens.

Je ne sais pas pourquoi c'est du bon sens. Ceci est le code de notre base de code actuellement. Nous avons un décorateur qui ajoute une méthode au prototype d'une classe. Erreurs TypeScript en sortie. À l'époque, je viens de jeter cette déclaration là-dessus. J'aurais pu utiliser la fusion d'interface.

Mais ce serait bien de ne rien utiliser du tout. Ce serait vraiment, vraiment bien de pouvoir dire à TypeScript que "ce décorateur ici ajoute cette méthode à la classe à laquelle elle est appliquée - autorisez this à l'intérieur de la classe à y accéder aussi"

Beaucoup d'autres dans ce fil semblent utiliser des décorateurs de la même manière, donc j'espère que vous considérerez qu'il n'est peut-être pas logique que cela ne fonctionne pas.

image

@arackaf Ouais, donc tu devrais faire export class SearchVm extends (@mappable({...}) class extends SearchVmBase {}) . C'est SearchVm qui dépend de la mappabilité, et non l'inverse.

export classe SearchVm étend (@mappable({...}) classe étend SearchVmBase {})

Tout l'intérêt de ce fil est d'ÉVITER le passe-partout comme ça. Les décorateurs fournissent un DX beaucoup, beaucoup plus agréable que d'avoir à faire des choses comme ça. Il y a une raison pour laquelle nous avons choisi d'écrire du code comme moi, plutôt que cela.

Si vous lisez les commentaires de ce fil, j'espère que vous serez convaincu que beaucoup d'autres personnes essaient d'utiliser la syntaxe plus simple du décorateur, et donc de reconsidérer ce que nous "devons" faire, ou ce qui est "bon sens", etc.

Ce que vous voulez n'a rien à voir avec les décorateurs en particulier. Votre problème est que le mécanisme de TypeScript pour dire "hé TypeScript, il se passe des choses dans cette structure dont vous ne savez rien, laissez-moi vous en parler", est actuellement trop limité. Pour le moment, nous n'avons que la fusion d'interfaces, mais vous voulez que les fonctions puissent appliquer la fusion d'interfaces à leurs arguments, ce qui n'est en aucun cas spécifiquement lié aux décorateurs.

Pour le moment, nous n'avons que la fusion d'interfaces, mais vous voulez que les fonctions puissent appliquer la fusion d'interfaces à leurs arguments, ce qui n'est en aucun cas spécifiquement lié aux décorateurs.

D'accord. Assez juste. Voulez-vous que j'ouvre un sujet séparé, ou voulez-vous renommer celui-ci, etc. ?

@arackaf Le seul "passe-partout" est le fait que j'avais class { } , qui est a) 7 caractères fixes, peu importe le nombre de décorateurs que vous utilisez b) le résultat du fait que les décorateurs ne peuvent pas (encore ) être appliqué à des expressions de retour de classe arbitraires, et c) parce que je voulais spécifiquement utiliser des décorateurs à votre avantage. Cette formulation alternative :

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

n'est pas plus verbeux que ce que vous faites à l'origine.

D'accord. Assez juste. Voulez-vous que j'ouvre un sujet séparé, ou voulez-vous renommer celui-ci, etc. ?

Un problème séparé serait bien.

n'est pas plus verbeux que ce que vous faisiez à l'origine

Ce n'est pas plus verbeux, mais j'espère vraiment que vous considérerez le fait que beaucoup n'aiment tout simplement pas cette syntaxe. Pour le meilleur ou pour le pire, beaucoup préfèrent l'offre des décorateurs DX.

Un problème séparé serait bien.

Je vais en taper un demain et le lier à celui-ci pour le contexte.

Merci d'avoir aidé à résoudre ce problème :)

Si je peux me permettre, @masaeedu je ne suis pas d'accord avec cette déclaration :

Ce que vous voulez n'a rien à voir avec les décorateurs en particulier. Votre problème est que le mécanisme de TypeScript pour dire "hé TypeScript, il se passe des choses dans cette structure dont vous ne savez rien, laissez-moi vous en parler", est actuellement trop limité. Pour le moment, nous n'avons que la fusion d'interfaces, mais vous voulez que les fonctions puissent appliquer la fusion d'interfaces à leurs arguments, ce qui n'est en aucun cas spécifiquement lié aux décorateurs.

TypeScript pourrait sûrement examiner le type de retour du décorateur de classe pour déterminer le type que devrait être la classe mutée. Il n'est pas nécessaire "d'appliquer la fusion d'interface aux arguments". Donc, comme je le vois, c'est entièrement à voir avec les décorateurs.

De plus, le cas où TypeScript ne sait pas qu'un composant connecté ne nécessite plus d'accessoires est un aspect qui m'a dissuadé d'utiliser Redux avec React et TypeScript.

@codeandcats Je tourne en rond avec ça depuis le début du fil, et je ne peux vraiment plus le faire. Veuillez lire attentivement la discussion précédente et essayez réellement de comprendre ce sur quoi je n'étais pas d'accord avec @arackaf .

Je suis d'accord que lorsque vous faites <strong i="8">@bar</strong> class Foo { } , le type de Foo devrait être le type de retour de bar . Je ne suis pas d'accord que this à l'intérieur Foo devrait être affecté de quelque façon que ce soit. L'utilisation connect est totalement hors de propos ici, car connect ne s'attend pas à ce que le composant décoré connaisse et utilise les membres qu'il ajoute au prototype.

Je peux comprendre que vous soyez frustré @masaeedu mais ne supposez pas simplement parce que je n'ai pas activement exprimé mon opinion dans ce va-et-vient que vous avez eu au cours des 48 dernières heures que je n'ai pas suivi la discussion - alors s'il vous plaît épargnez-moi la condescendance mon pote.

Si je comprends bien, vous pensez qu'à l'intérieur d'une classe décorée, elle ne devrait pas être au courant des mutations faites sur elle-même par les décorateurs, mais à l'extérieur, elle devrait le faire. Je ne suis pas en désaccord avec cela. Le fait que @arackaf pense qu'à l'intérieur d'une classe devrait voir la version mutée est d'ailleurs le but de mon commentaire.

Dans les deux cas, TypeScript pourrait utiliser le type renvoyé par une fonction de décorateur comme source de vérité pour une classe. Et par conséquent, il s'agit d'un problème de décorateur, pas d'une nouvelle fonctionnalité de mutation d'argument bizarre comme vous le suggérez, qui ressemble honnêtement à une tentative d'homme de paille.

@Zalastax qui utilise la fonction de décorateur en tant que fonction, pas en tant que décorateur. Si vous avez plusieurs décorateurs que vous devez appliquer, vous devez les imbriquer, ce qui n'est techniquement plus verbeux mais n'est pas aussi syntaxiquement agréable - vous voyez ce que je veux dire ?

condescendance etc...

Bruit.

Le fait que @arackaf pense qu'à l'intérieur d'une classe devrait voir la version mutée est d'ailleurs le but de mon commentaire.

C'est le point central du paragraphe auquel vous répondez, donc si vous parlez d'autre chose, cela n'est pas pertinent. Ce que @arackaf veut, c'est, en gros, une fusion d'interface sur des arguments de fonction. Ceci est mieux traité par une fonctionnalité indépendante des décorateurs. Ce que vous voulez (ce qui est apparemment identique à ce que je veux), c'est que le bogue dans TypeScript soit corrigé où <strong i="12">@foo</strong> class Foo { } n'entraîne pas la même signature de type pour Foo que const Foo = foo(class { }) . Seul ce dernier est spécifiquement lié aux décorateurs.

Si je comprends bien, vous pensez qu'à l'intérieur d'une classe décorée, elle ne devrait pas être au courant des mutations faites sur elle-même par les décorateurs, mais à l'extérieur, elle devrait le faire. Je ne suis pas d'accord avec ça

Si vous avez décidé de parler de quelque chose sur lequel nous sommes d'accord, mais que vous le préfacez par "Je ne suis pas d'accord avec cette affirmation", alors vous perdez simplement du temps.

Puis-je avoir des frites avec ce sel ?

Oh, je vois, quand vous fréquentez vos pairs, c'est bien, mais si les gens vous tirent dessus, c'est du bruit.

Je ne vois pas pourquoi votre concept de "mutation d'argument" est nécessaire pour mettre en œuvre ce que @arackaf a proposé.

Que nous soyons d'accord avec @arackaf ou non, dans tous les cas, TypeScript pourrait simplement déduire le type réel du résultat du décorateur, à mon humble avis.

Toutes mes excuses si vous vous êtes senti condescendant ; l'intention était de vous amener à
en fait un fil de discussion géant pour lequel tout le monde pourrait être pardonné
écrémage. Je m'en tiens à cela, car il est encore assez clair que vous n'êtes pas au courant
des détails et sont "en désaccord" avec moi sur la base d'un malentendu
ma position.

Par exemple, votre question sur la raison pour laquelle l'interface fusionne sur la fonction
arguments résout le problème d'Adam est mieux résolu en examinant les arguments pertinents
discussion avec Adam, où il dit qu'il utilise déjà la fusion d'interface,
mais trouve ennuyeux de devoir le faire à chaque site de déclaration.

Je pense que tous les aspects techniques de cela ont été battus à mort. je
Je ne veux pas que le fil soit dominé par des querelles interpersonnelles, donc cela
va être mon dernier message.

C'est vrai, il y a deux bugs avec les décorateurs : le type de retour n'est pas respecté, et interne à la classe, les membres ajoutés ne sont pas respectés ( this à l'intérieur de la classe). Et oui, je suis plus préoccupé par ce dernier, car le premier est plus facile à contourner.

J'ai ouvert un cas séparé ici : https://github.com/Microsoft/TypeScript/issues/16599

Salut tout le monde, j'ai raté cette conversation ce week-end, et peut-être n'y a-t-il pas une tonne de raisons de la reprendre maintenant ; mais je tiens à rappeler à tout le monde que dans le feu de l'action de ce genre de discussions, tous ceux qui interviennent sont généralement bien intentionnés. Garder un ton respectueux peut aider à clarifier et à orienter la conversation, et peut encourager de nouveaux contributeurs. Faire cela n'est pas toujours facile (surtout quand Internet laisse la tonalité au lecteur), mais je pense que c'est important pour les scénarios futurs. 😃

Où en est-on ?

@alex94puchades c'est toujours une proposition d'étape 2 , donc nous avons probablement encore un moment. TC39 semble avoir du mouvement , au moins.

D'après ce commentaire , il semble qu'il pourrait être proposé pour l'étape 3 dès novembre.

une solution de contournement pour changer la signature d'une fonction par décorateur

ajouter une fonction wapper vide

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

ajouter une définition

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

résultat

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

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

/ping

Si quelqu'un cherche une solution à ce problème, une solution de contournement que j'ai trouvée est ci-dessous.

Ce n'est pas la meilleure solution car elle nécessite un objet imbriqué, mais pour moi, cela fonctionne bien car je voulais en fait que les propriétés du décorateur soient dans un objet et pas seulement à plat sur l'instance de classe.

C'est quelque chose que j'utilise pour mes modaux Angular 5.

Imaginez que j'ai un décorateur @ModalParams(...) que je peux utiliser sur ConfirmModalComponent personnalisé. Pour que les propriétés du décorateur @ModalParams(...) apparaissent dans mon composant personnalisé, je dois étendre une classe de base qui a une propriété à laquelle le décorateur attribuera ses valeurs.

Par exemple:

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 
    }
}

Encore une fois, ce n'est pas très joli, mais cela fonctionne bien pour mon cas d'utilisation, alors j'ai pensé que quelqu'un d'autre pourrait le trouver utile.

@lansana mais vous ne comprenez pas les types, n'est-ce pas ?

@confraria Malheureusement non, mais il peut y avoir un moyen d'y parvenir si vous implémentez une classe générique Modal que vous étendez. Par exemple, quelque chose comme ceci pourrait fonctionner (non testé):

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

:/ oui mais alors le type est découplé du décorateur et vous ne pouvez certainement pas en utiliser deux .. :( en plus vous obtiendrez des erreurs si la classe n'implémente pas les méthodes .. :( je ne pense pas qu'il y ait est un moyen d'obtenir les bons types avec ce modèle pour le moment

Oui, ce serait formidable si cela était possible, rend TypeScript encore meilleur et expressif. Espérons que quelque chose en sortira bientôt.

@lansana Ouais, le fait est qu'il serait bien que les décorateurs de classe soient capables de changer la signature de la classe par eux-mêmes, sans obliger la classe à étendre ou à implémenter quoi que ce soit d'autre (puisque c'est une duplication d'effort et d'informations de type) .

Remarque : dans votre exemple, n'oubliez pas que params serait statique sur toutes les instances de classes de composants modaux décorés, car il s'agit d'une référence d'objet. Bien que ce soit peut-être par conception. : - ) Mais je m'égare...

Edit : en y réfléchissant, je peux voir les inconvénients de permettre aux décorateurs de modifier les signatures de classe. Si l'implémentation de la classe comporte clairement certaines annotations de type, mais qu'un décorateur est capable d'intervenir et de changer tout cela, ce serait un peu une mauvaise astuce à jouer aux développeurs. De nombreux cas d'utilisation concernent évidemment l'incorporation d'une nouvelle logique de classe, donc malheureusement beaucoup d'entre eux seraient mieux facilités par l'implémentation d'une extension ou d'une interface - qui se coordonne également avec la signature de classe existante et génère des erreurs appropriées en cas de collision. Un framework comme Angular fait bien sûr un usage abondant des décorateurs pour augmenter les classes, mais la conception n'est pas de permettre à la classe de "gagner" ou de mélanger une nouvelle logique du décorateur qu'elle peut ensuite utiliser dans sa propre implémentation - c'est pour isoler logique de classe à partir de la logique de coordination du cadre. C'est mon _humble_ opinion, en tout cas. :-)

Il semble préférable d'utiliser simplement des classes d'ordre supérieur plutôt que des décorateurs + des hacks. Je sais que les gens veulent utiliser des décorateurs pour ce genre de choses, mais utiliser composer + HOCs est la voie à suivre et cela restera probablement ainsi... pour toujours ;) Selon MS, etc., les décorateurs servent uniquement à attacher des métadonnées aux classes et lorsque vous vérifiez les grands utilisateurs de décorateurs tels que Angular, vous verrez qu'ils ne sont utilisés qu'à ce titre. Je doute que vous puissiez convaincre les responsables de TypeScript du contraire.

Il est triste et un peu étrange qu'une fonctionnalité aussi puissante, qui permettrait une véritable composition de fonctionnalités, et qui génère un tel engagement, ait été ignorée par l'équipe TS depuis si longtemps maintenant.

C'est vraiment une fonctionnalité qui pourrait révolutionner la façon dont nous écrivons du code ; permettant à chacun de publier de petits mixins sur des choses comme Bitly ou NPM et d'avoir une réutilisation de code vraiment géniale dans Typescript. Dans mes propres projets, je ferais instantanément mon @Poolable @Initable @Translating et probablement une pile de plus.

S'il vous plaît toute la puissante équipe de base de TS. "Tout ce dont vous avez besoin" pour implémenter est que les interfaces renvoyées doivent être honorées.

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

ce qui permettrait à ce code de s'exécuter sans plainte :

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

@TousNomsRTaken

Bien que je sois d'accord avec vos points, vous pouvez obtenir la même chose comme suit :

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

Et puis vous n'auriez même pas besoin d'une fonction init, vous pourriez simplement faire ceci :

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

@lansana
Ouais mais ça polluerait mon constructeur qui n'est pas toujours super duper ce que je veux.

J'ai aussi un mixin similaire pour rendre n'importe quel objet Poolable et d'autres choses. Juste la possibilité d'ajouter des fonctionnalités grâce à la composition est ce qui est nécessaire. L'exemple init est juste un moyen nécessaire de faire ce que vous avez fait sans polluer le constructeur, ce qui le rend utile avec d'autres frameworks.

@AllNamesRTaken Il ne sert à rien de toucher les décorateurs en ce moment car il est dans cet état depuis si longtemps et la proposition est déjà au stade 2. Attendez que https://github.com/tc39/proposal-decorators soit finalisé, puis nous les obtiendrons très probablement sous la forme sur laquelle tout le monde sera d'accord.

@Kukkimonsuta
Je ne suis pas du tout d'accord avec vous car ce que je demande concerne uniquement le type et non la fonctionnalité. Par conséquent, cela n'a pas grand-chose à voir avec les trucs ES. Ma solution ci-dessus fonctionne déjà, je ne veux tout simplement pas avoir à caster pour.

@AllNamesRTaken , vous pouvez le faire _aujourd'hui_ avec des mixins sans aucun avertissement :

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

À l'avenir, si nous obtenons la proposition de mixins dans JS, nous pouvons le faire :

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 était la façon dont j'ai commencé avec ces fonctionnalités et ma mise en œuvre fonctionne également comme vous le décrivez :

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

ce qui est sympa, comme solution de contournement, mais n'enlève pas vraiment le mérite de permettre aux décorateurs de changer le type à mon avis.

EDIT : il perd également la frappe sur le partiel pour init.

Avec cette fonctionnalité, vous pourrez écrire HoC plus facilement
J'espère que cette fonctionnalité sera ajoutée

@kgtkr c'est la principale raison pour laquelle je le veux tellement...

Il y a aussi une petite urgence dans les définitions react-router parce que certaines personnes ont décidé que la sécurité des types est plus importante que d'avoir une interface de décoration de classe. La raison principale est que withRouter rend certains accessoires facultatifs.

Maintenant, il semble y avoir une querelle, où les gens sont obligés d'utiliser l'interface de fonction de withRouter au lieu de decoration .

Commencer à résoudre cette fonctionnalité rendrait le monde plus heureux.

La même querelle * s'est produite il y a quelque temps avec les typages material-ui et withStyle , qui a été conçu pour être utilisé comme décorateur, mais qui, pour être utilisé de manière sécurisée, les utilisateurs de TypeScript doivent utiliser comme une fonction régulière. Même si la poussière s'est principalement installée là-dessus, c'est une source de confusion permanente pour les nouveaux venus dans le projet !

* Eh bien, "querelle" peut être un mot fort

Je regarde cela depuis longtemps, et jusqu'à ce qu'il arrive, d'autres pourront, espérons-le, bénéficier de mon petit hack pour obtenir ce comportement d'une manière compatible. Alors voilà, implémentant une méthode de classe de cas de style Scala super basique copy ...

J'ai le décorateur implémenté comme ceci:

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

Ce code crée une classe anonyme avec la méthode copy attachée. Cela fonctionne exactement comme prévu en JavaScript dans un environnement prenant en charge les décorateurs.

Pour l'utiliser dans TypeScript et faire en sorte que le système de type reflète la nouvelle méthode dans la classe ciblée, le hack suivant peut être utilisé :

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

Toutes les instances de MyCaseClass exposeront les typages pour la méthode héritée copy de la classe anonyme à l'intérieur du décorateur CaseClass . Et, lorsque TypeScript prend en charge la mutation des types déclarés, ce code peut être modifié facilement à la syntaxe de décorateur habituelle <strong i="18">@CaseClass</strong> etc sans aucun problème inattendu.

Ce serait formidable de voir cela dans la prochaine version majeure de TypeScript - je pense que cela aidera un code plus propre et plus concis au lieu d'exporter un désordre étrange de classes proxy.

Quand cette fonctionnalité sera-t-elle disponible ?

Je voulais utiliser un décorateur de classe pour m'occuper des tâches répétées.
Mais maintenant, lors de l'initialisation de la classe, j'obtiens l'erreur suivante : 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.

J'espère que cette fonctionnalité sera bientôt disponible ! :)

@iainreid820 avez-vous rencontré des bogues étranges en utilisant cette approche ?

La longue attente ! En attendant, cela est-il résolu par quoi que ce soit sur la feuille de route actuelle ?
Comme le numéro 5453 ?

J'utilise Material UI et je devais juste réaliser que TypeScript ne prend pas en charge la syntaxe du décorateur pour withStyles : https://material-ui.com/guides/typescript/#decorating -components

Veuillez corriger cette limitation dans la prochaine version de TypeScript. Les décorateurs de classe me semblent assez inutiles en ce moment.

En tant que mainteneur de Morphism Js , c'est une grande limitation pour ce type de bibliothèque. J'aimerais éviter au consommateur d'un décorateur de fonction d'avoir à spécifier le type cible de la fonction https://github.com/nobrainr/morphism# --toclassobject-decorator, sinon utiliser des décorateurs au lieu de HOF semble un peu inutile 😕
Existe-t-il un plan pour résoudre ce problème ? Existe-t-il un moyen d'aider à créer cette fonctionnalité ? Merci d'avance!

@bikeshedder cet exemple d'interface utilisateur matérielle est un cas d'utilisation de mixin de classe, et vous pouvez obtenir le bon type à partir de mixins. À la place de:

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

écrivez:

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

@justinfagnani Cela ne fonctionne pas pour moi :

ss 2018-10-16 at 10 00 47

Voici mon code : https://gist.github.com/G-Rath/654dff328dbc3ae90d16caa27a4d7262

@G-Rath Je pense que new () => React.Component<Props, State> devrait plutôt fonctionner?

@emyann Pas de dés. J'ai mis à jour l'essentiel avec le nouveau code, mais est-ce ce que vous vouliez dire ?

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

Peu importe comment vous le formatez, extends withStyles(styles)(...) ne ressemble pas à une suggestion appropriée, fondamentalement. withStyles n'est PAS un mixage de classe.

withStyles prend la classe de composant A et crée la classe B qui, une fois rendue, rend la classe A et lui transmet les accessoires + l'accessoire classes .

Si vous étendez la valeur de retour de withStyles, vous étendez le wrapper B , au lieu d'implémenter la classe A qui reçoit en fait le prop classes .

Peu importe comment vous le formatez, s'étend withStyles(styles)(...) ne ressemble pas à une suggestion appropriée, fondamentalement. withStyles n'est PAS un mixage de classe.

En effet. Je ne sais pas pourquoi il y a tant de recul ici.

Cela dit, les décorateurs sont extrêmement proches de l'étape 3, d'après ce que j'entends, donc j'imagine que TS recevra bientôt un soutien approprié ici.

J'entends les gens qui veulent une syntaxe plus propre, en particulier. lors de l'utilisation de plusieurs HoC en tant que décorateurs. La solution de contournement temporaire que nous utilisons consiste à envelopper plusieurs décorateurs dans une fonction de canal, puis à utiliser cette fonction pure pour décorer le composant. par exemple

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

flow est lodash.flow . De nombreuses bibliothèques d'utilitaires fournissent quelque chose comme ceci - recompose , Rx.pipe etc. mais vous pouvez bien sûr écrire votre propre fonction de pipe simple si vous ne voulez pas installer de bibliothèque.

Je trouve cela plus facile à lire que de ne pas utiliser de décorateurs,

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

Une autre raison pour laquelle j'utilise ceci, c'est que ce modèle est facilement trouvable et remplaçable/grepable une fois que la spécification du décorateur est annoncée et correctement prise en charge.

@justinfagnani Bizarrement, j'obtiens une erreur de syntaxe d'ESLint lorsque j'essaie de changer extends React.Component<Props, State> en extends withStyles(styles)(React.Component<Props, State>) , donc je n'ai pas pu vérifier l'erreur de type de @ G-Rath de la même manière.

Mais j'ai remarqué que si je fais quelque chose comme suit, un problème avec new (peut-être le même problème ?) apparaît :

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

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

et l'erreur est :

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

Cela signifie-t-il que le type renvoyé par Material UI n'est pas un constructeur (bien qu'il devrait l'être) ?

@sagar-sm Mais, obtenez-vous le bon type augmenté du type Material UI? Il ne semble pas que vous le ferez.

Pour référence, je suis tombé là-dessus parce que j'essayais également d'utiliser withStyles de Material UI en tant que décorateur, ce qui n'a pas fonctionné, j'ai donc posé cette question: https://stackoverflow.com/questions/53138167

besoin de cela, alors nous pouvons faire en sorte que la fonction asynchrone revienne en tant que bluebird

J'ai essayé de faire quelque chose de similaire. ATM J'utilise la solution de contournement suivante :

Voici d'abord mon décorateur "mixin"

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>
        }
    }
}

Créer un décorateur à partir de l'interface

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

Et c'est comme ça que je l'utilise :

@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)

L'astuce consiste également à créer une interface avec le même nom que la classe pour créer une déclaration fusionnée.
La classe ne s'affiche toujours que sous le type Test mais les vérifications à partir de dactylographie fonctionnent.
Si vous utilisiez le décorateur sans la @ -Notation et que vous l'appeliez simplement en tant que Function, vous obtiendriez le bon type d'intersection mais perdriez la possibilité de vérifier le type à l'intérieur de la classe elle-même puisque vous ne pouvez pas utiliser le truc d'interface plus et il semble plus moche. Par exemple:

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)

Que pensez-vous de ces approches ? Ce n'est pas beau mais ça marche aussi loin qu'une solution de contournement.

Y a-t-il des progrès sur cette question? Cela semble être une fonctionnalité très, très puissante qui ouvrirait de nombreuses possibilités différentes. Je suppose que ce problème est pour la plupart obsolète en ce moment parce que les décorateurs sont encore expérimentaux ?

Ce serait vraiment bien d'avoir une déclaration officielle sur ce @andy-ms @ahejlsberg @sandersn. 🙏

Nous ne ferons probablement rien ici tant que les décorateurs ne seront pas finalisés.

@DanielRosenwasser - que signifie "finalisé", ici ? L'étape 3 du TC39 est-elle éligible ?

Je n'avais pas réalisé qu'ils étaient allés si loin ! Quand ont-ils atteint le stade 3 ? C'est récent ?

Ils ne l'ont pas encore fait; Je crois qu'ils sont prêts à passer de l'étape 2 à l'étape 3 à nouveau lors de la réunion du TC39 de janvier.

Vous pouvez garder un œil sur l'agenda pour plus de détails.

Correct (bien que je ne puisse pas confirmer qu'il est en place pour l'avancement en janvier). Je demandais seulement si l'étape 3 se qualifierait quand ils y arriveraient.

À mon avis, la proposition de décorateur a encore de nombreux problèmes sérieux, donc si elle passe à l'étape 3 en janvier, elle fera à nouveau l'erreur, tout comme la proposition de champs de classe problématique et la proposition globalThis.

@hax pourriez-vous élaborer?
Je le voudrais vraiment vraiment et je trouve le manque de communication à ce sujet attristant et je n'ai malheureusement pas entendu parler des problèmes.

@AllNamesRTaken Vérifiez la liste des problèmes de proposition de décorateur. 😆 Par exemple, l'argument décorateur export avant/après est un bloqueur de l'étape 3.

Il y a aussi quelques changements proposés comme la refonte de l'API qui, je crois, signifient que la proposition n'est pas stable pour l'étape 3.

Et cela était également lié à la proposition problématique des champs de classe. Bien que les champs de classe aient atteint le stade 3, il y a trop de problèmes. Un gros problème qui me préoccupe est qu'il laisse de nombreux problèmes aux décorateurs (par exemple, protected ) et c'est mauvais pour les deux propositions.

Notez que je ne suis pas des délégués du TC39, et certains délégués du TC39 ne sont jamais d'accord avec mes commentaires sur l'état actuel de nombreux problèmes. (En particulier, j'ai la ferme opinion que le processus actuel du TC39 présente de gros échecs sur de nombreuses questions controversées. Reporter certaines questions aux décorateurs ne résoudra jamais vraiment le problème, il suffit de rendre la proposition du décorateur plus fragile.)

Je pense que ces choses seront résolues et que la proposition ira bien, mais je préférerais que nous ne discutions pas de l'état de la proposition ici.

Je pense qu'une étape 3 avec une confiance suffisante ou une étape 4 est probablement celle où nous mettrons en œuvre la nouvelle proposition, puis nous pourrions examiner cette question.

Merci Daniel ! L'étape 3 implique généralement une forte confiance en 4, alors croisons les doigts pour la réunion de janvier.

Et merci d'avoir dirigé la discussion des décorateurs ici. C'est bizarre le niveau de colère et de perte de vélo que cette fonctionnalité a causé. Je n'ai jamais rien vu de tel 😂

juste pour mémoire, il y avait une demande de fonctionnalité sur la même chose : https://github.com/Microsoft/TypeScript/issues/8545

assez ennuyeux TypeScript prend en charge cette fonctionnalité dans une certaine mesure lorsque vous compilez à partir de JavaScript (https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#better-handling-for-namespace-patterns-in -js-fichiers) :

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

et même pour le code TypeScript lui-même, mais uniquement en ce qui concerne les fonctions (!) (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 Un mot sur la proposition de passer à l'étape 3 ? Je cherche à construire une bibliothèque qui ajoute des fonctions de prototype à une classe lors de l'utilisation d'un décorateur.

Ils n'ont pas avancé lors de la dernière réunion du TC39 ; ils peuvent être de nouveau en lice pour l'avancement lors de la prochaine réunion. Cependant, ce n'est pas clair; il y a beaucoup de choses dans l'air au sujet de la proposition à ce stade.

Les gens ici qui sont intéressés seraient bien avisés de suivre la proposition elle-même - ce qui réduira le bruit pour ceux d'entre nous qui regardent le fil ainsi que pour les mainteneurs de TS.

des mises à jour sur celui-ci?

solution de contournement (angulaire :)) 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
    }
}

Que pensez-vous de cette solution facile « en attendant » ?
https://stackoverflow.com/a/55520697/1053872

Mobx-state-tree utilise une approche similaire avec leur fonction cast() . Ce n'est pas agréable mais... Cela ne me dérange pas trop non plus. Il est facile de retirer chaque fois que quelque chose de mieux se présente.

J'aimerais juste pouvoir faire quelque chose dans ce sens ❤️
N'est-ce pas le pouvoir que les décorateurs sont censés avoir ?

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

J'ai passé pas mal de temps à créer une solution de contournement pour taper des propriétés de classe décorées. Je ne pense pas que ce sera une solution viable aux scénarios de ce fil, mais vous pouvez en trouver certaines parties utiles. Si vous êtes intéressé, vous pouvez consulter mon article sur Medium

Des mises à jour sur ce problème ?

Comment on fait avec ça ?

Quel résultat de ce problème, comment ça marche maintenant?

Vous pouvez utiliser mixin-classes pour obtenir cet effet
https://mariusschulz.com/blog/mixin-classes-in-typescript

@Bnaya qui n'est absolument pas ce que nous voulons maintenant ;).
Les décorateurs nous ont permis d'éviter de créer des classes que nous n'avons jamais l'intention d'utiliser comme nous faisions du Java à l'ancienne. Les décorateurs permettraient une architecture de composition très propre et ordonnée où la classe peut continuer à faire les fonctionnalités de base et avoir des fonctionnalités généralisées supplémentaires composées dessus. Ouais les mixins pourraient le faire mais c'est un pansement.

La proposition de décorateur a considérablement changé au cours des derniers mois - il est hautement improbable que l'équipe TS investisse à tout moment dans l'implémentation actuelle qui sera bientôt incompatible™.

Je vous recommande de regarder les ressources suivantes :

Proposition de décorateurs : https://github.com/tc39/proposal-decorators
Feuille de route TypeScript : https://github.com/microsoft/TypeScript/wiki/Roadmap

@Bnaya qui n'est absolument pas ce que nous voulons maintenant ;).
Les décorateurs nous ont permis d'éviter de créer des classes que nous n'avons jamais l'intention d'utiliser comme nous faisions du Java à l'ancienne. Les décorateurs permettraient une architecture de composition très propre et ordonnée où la classe peut continuer à faire les fonctionnalités de base et avoir des fonctionnalités généralisées supplémentaires composées dessus. Ouais les mixins pourraient le faire mais c'est un pansement.

Ce n'est pas un mélange, mais une classe.
Ce n'est pas le mixin de mauvaise copie qui
Lorsque l'opérateur du pipeline arrivera, la syntaxe sera également raisonnable

Vous pouvez utiliser mixin-classes pour obtenir cet effet

Les mixins de classe peuvent être pénibles dans TypeScript ; même s'ils ne l'étaient pas, ils sont très passe-partout en vous obligeant à encapsuler des classes dans une fonction pour les convertir en mixin, ce que vous ne pouvez parfois pas faire simplement si vous importez des classes tierces.

Ce qui suit est bien meilleur, plus simple, plus propre et fonctionne avec des classes tierces importées. Je l'ai fait fonctionner correctement en JavaScript simple sans transpilation autre que pour transpiler le décorateur de style hérité, mais les class es restent totalement natifs 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()

Et la sortie dans Chrome est :

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

Cela vous donne complètement ce que font les mixins d'usine de classe, sans tout le passe-partout. Les wrappers de fonction autour de vos classes ne sont plus nécessaires.

Cependant, comme vous le savez, le décorateur ne peut pas modifier le type de la classe définie. 😢

Donc, pour le moment, je peux faire ce qui suit, la seule mise en garde étant que les membres protégés ne peuvent pas être hérités :

// `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) }
}

Le problème de la perte de membres protégés est illustré par ces deux exemples d'implémentations multiple (en ce qui concerne les types, mais l'implémentation d'exécution est omise par souci de brièveté) :

  • exemple sans erreur , car toutes les propriétés des classes combinées sont publiques.
  • exemple avec erreur (tout en bas), car les types mappés ignorent les membres protégés, dans ce cas rendant Two.prototype.two non héritable.

J'aimerais vraiment trouver un moyen de mapper les types, y compris les membres protégés.

Cela me serait vraiment utile car j'ai un décorateur qui permet d'appeler une classe en tant que fonction.

Cela marche:

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

Cela ne fonctionne pas :

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

Une solution de contournement pas si difficile à nettoyer lorsque cette fonctionnalité est disponible peut être effectuée à l'aide de la fusion de déclarations et d'un fichier de définition de types personnalisés.

Ayant déjà ça :

// 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 {
  // ...
}

Ajoutez le fichier suivant (fichier à supprimer une fois la fonctionnalité sortie) :

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

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

Une autre solution de contournement possible

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

Commencé en 2015 et toujours en attente. Il existe de nombreux autres problèmes connexes et même des articles comme celui-ci https://medium.com/p/caf24aabcb59/responses/show , essayant de montrer des solutions de contournement (qui sont un peu hacky, mais utiles)

Est-il possible pour quelqu'un de faire la lumière là-dessus. Est-il encore envisagé de faire l'objet d'une discussion interne ?

tl;dr - cette fonctionnalité est bloquée dans TC39. Je ne peux pas du tout blâmer les gens de TS pour cela. Ils ne vont pas l'implémenter tant que ce ne sera pas standard.

Mais c'est Microsoft, ils peuvent juste l'implémenter et ensuite dire aux standardistes - "vous voyez, les gens utilisent déjà notre version" :trollface:

Le problème ici est que la proposition de décorateurs a changé de manière significative (de manière rétrocompatible) depuis que TypeScript a implémenté les décorateurs. Cela va conduire à une situation douloureuse lorsque les nouveaux décorateurs seront finalisés et implémentés, car certaines personnes s'appuient sur le comportement et la sémantique actuels des décorateurs de TypeScript. Je soupçonne fortement que l'équipe TypeScript souhaite éviter de résoudre ce problème, car cela encouragerait une utilisation accrue des décorateurs non standard actuels, ce qui rendrait finalement la transition vers toute nouvelle implémentation de décorateur encore plus douloureuse. Essentiellement, personnellement, je ne vois tout simplement pas cela se produire.

Incidemment, j'ai récemment créé # 36348, qui est une proposition de fonctionnalité qui fournirait une solution de contournement assez solide. N'hésitez pas à jeter un coup d'œil/à donner votre avis/sauvage. 🙂

tl;dr - cette fonctionnalité est bloquée dans TC39. Je ne peux pas du tout blâmer les gens de TS pour cela. Ils ne vont pas l'implémenter tant que ce ne sera pas standard.

Compte tenu de ce fait, il serait peut-être judicieux que l'équipe de développement rédige une explication rapide et une mise à jour du statut, et verrouille cette conversation jusqu'à ce que TC39 fasse avancer cela à l'étape requise ?

Quelqu'un qui lit ceci a-t-il un poids en la matière?

En fait, à peu près toutes les conversations liées aux décorateurs sont suspendues dans les airs. Exemple : https://github.com/Microsoft/TypeScript/issues/2607

Obtenir des éclaircissements sur l'avenir des décorateurs va être utile, même s'il s'agit de demander aux contributeurs d'implémenter les fonctionnalités requises. Sans orientation claire, l'avenir des décorateurs est flou

J'aimerais que l'équipe TypeScript puisse assumer un rôle proactif pour la proposition de décorateur, de la même manière qu'elle a aidé à faire passer le chaînage facultatif. J'aimerais aussi aider, mais je n'ai pas encore travaillé dans le développement du langage et je ne sais donc pas comment le faire au mieux :-/

Je ne peux pas parler au nom de toute l'équipe et de TypeScript en tant que projet - mais je pense qu'il est peu probable que nous fassions un nouveau travail de décorateur tant qu'il n'est pas réglé et confirmé, étant donné que l'implémentation du décorateur a déjà divergé une fois de l'implémentation de TypeScript.

La proposition est toujours en développement actif et a été présentée au TC39 cette semaine https://github.com/tc39/proposal-decorators/issues/305

@orta dans ce cas, je pense que la documentation sur le décorateur de classe devrait alors être mise à jour. Il stipule que

Si le décorateur de classe renvoie une valeur, il remplacera la déclaration de classe par la fonction constructeur fournie.

De plus, l'exemple suivant de la documentation semble impliquer que le type d'instance Greeter aurait la propriété newProperty , ce qui n'est pas vrai :

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

Je pense qu'il vaut la peine d'ajouter à la documentation que l'interface de la classe renvoyée ne remplacera pas celle d'origine.

Intéressant, j'ai testé un tas d'anciennes versions de TypeScript et je n'ai pas trouvé d'occasion où cet exemple de code fonctionnait ( exemple de 3.3.3 ) - alors oui, je suis d'accord.

J'ai créé https://github.com/microsoft/TypeScript-Website/issues/443 - si quelqu'un sait comment faire en sorte que ce classDecorator ait un type d'intersection explicite, veuillez commenter dans ce numéro

Je me suis retrouvé ici parce que le problème avec la documentation répertoriée ci-dessus m'a causé de la confusion. Bien qu'il serait formidable que la signature de type de la valeur renvoyée par le décorateur soit reconnue par le compilateur TS, au lieu de ce changement plus important, je suis d'accord pour que la documentation soit clarifiée.

Pour mémoire, il s'agit d'une version simplifiée de ce que j'essayais dans un format les docs connexes du PR de @orta :

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);
Cette page vous a été utile?
0 / 5 - 0 notes