Typescript: Solicitud: mutación del decorador de clase

Creado en 20 sept. 2015  ·  231Comentarios  ·  Fuente: microsoft/TypeScript

Si podemos hacer que esto se verifique correctamente, tendríamos un soporte perfecto para mixins sin repeticiones:

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

Comentario más útil

Lo mismo sería útil para los métodos:

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

Se supone que el decorador asíncrono debe cambiar el tipo de retorno a Promise<any> .

Todos 231 comentarios

Lo mismo sería útil para los métodos:

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

Se supone que el decorador asíncrono debe cambiar el tipo de retorno a Promise<any> .

@Gaelan , ¡esto es exactamente lo que necesitamos aquí! Sería natural trabajar con mixins.

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 por esto. Aunque sé que esto es difícil de implementar, y probablemente más difícil llegar a un acuerdo sobre la semántica de mutación del decorador.

+1

Si el beneficio principal de esto es la introducción de miembros adicionales a la firma de tipo, ya puede hacerlo con la fusión de interfaz:

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

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

new Foo().foo();

Si el decorador es una función real que el compilador necesita invocar para mutar imperativamente la clase, esto no parece una cosa idiomática para hacer en un lenguaje de tipo seguro, en mi humilde opinión.

@masaeedu ¿Conoce alguna solución para agregar un miembro estático a la clase decorada?

@davojan Claro. Aqui tienes:

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

También sería útil poder introducir _múltiples_ propiedades a una clase al decorar un método (por ejemplo, un ayudante que genera un setter asociado para un getter, o algo por el estilo)

Los tipos de react-redux para connect toman un componente y devuelven un componente modificado cuyos accesorios no incluyen los accesorios conectados recibidos a través de redux, pero parece que TS no reconoce su definición connect como un decorador debido a este problema. ¿Alguien tiene una solución?

Creo que la definición de tipo ClassDecorator necesita cambiarse.

Actualmente es declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void; . Tal vez podría cambiarse a

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;

Obviamente, el nombre apesta y no tengo idea de si este tipo de cosas funcionarán (solo estoy tratando de convertir una aplicación de Babel a texto mecanografiado y estoy presionando esto).

@joyt ¿Podría proporcionar una reconstrucción del problema en el patio de recreo? No uso react-redux, pero como mencioné antes, creo que cualquier extensión que desee para la forma de un tipo se puede declarar mediante la fusión de interfaz.

@masaeedu aquí hay un desglose básico de las partes móviles...

Básicamente, el decorador proporciona un montón de accesorios al componente React, por lo que el tipo genérico del decorador es un subconjunto del componente decorado, no un superconjunto.

No estoy seguro de si esto es útil, pero traté de armar una muestra no ejecutable para mostrarle los tipos en juego.

// 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 desea un ejemplo de trabajo completo, le sugiero que baje https://github.com/jaysoo/todomvc-redux-react-typescript u otro proyecto de muestra de react/redux/typescript.

Según https://github.com/wycats/javascript-decorators#class -declaration, tengo entendido que el declare type WrappingClassDecorator = <TFunction extends Function, TDecoratorFunction extends Function>(target: TFunction) => TDecoratorFunction; propuesto no es válido.

La especificación dice:

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

se traduce a:

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

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

Entonces, si lo entiendo correctamente, lo siguiente debería ser cierto:

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

Para tu ejemplo:

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

Supongo que quiere decir que F devuelve una clase que se ajusta a X (y no es una instancia de X )? P.ej:

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

Para ese caso, las afirmaciones deben ser:

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

El decorador ha mutado el Foo que está dentro del alcance de esas declaraciones let . Ya no se puede acceder al Foo original. Es efectivamente equivalente a:

let Foo = F(class Foo {});

@nevir Sí, tienes razón. Gracias por la aclaración.

En una nota al margen, parece que desactivar la verificación para invalidar los tipos de clase mutados es relativamente fácil:

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

Pero no tengo los conocimientos suficientes para hacer que el compilador genere las definiciones de tipo correctas de la clase mutada. Tengo la siguiente prueba:

pruebas/casos/conformidad/decoradores/clase/decoradorOnClass10.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();

Genera 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

Yo estaba esperando
>C: typeof C para ser >C: typeof X | typeof Y

Para aquellos interesados ​​en connect de react-redux como estudio de caso para esta característica, he archivado https://github.com/DefinitelyTyped/DefinitelyTyped/issues/9951 para rastrear el problema en un solo lugar.

He leído todos los comentarios sobre este problema y tengo la idea de que la firma del decorador en realidad no muestra lo que puede hacer con la clase envuelta.

Considere este:

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

Debe escribirse de esa manera:
declare function decorator<T>(target: T): Wrapper<T>;

Pero esta firma no nos dice que el decorador haya agregado cosas nuevas al prototipo del objetivo.

Por otro lado, este no nos dice que el decorador en realidad ha devuelto un envoltorio:
declare function decorator<T>(target: T): T & { someMethod: () => void };

¿Alguna noticia sobre esto? ¡Esto sería súper poderoso para la metaprogramación!

¿Qué tal un enfoque más simple para este problema? Para una clase decorada, vinculamos el nombre de la clase al valor de retorno del decorador, como un azúcar sintáctico.

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.

En cuanto a la implementación, esto introducirá un símbolo sintético para la clase decorada. Y el nombre de la clase original solo está vinculado al alcance del cuerpo de la clase.

@HerringtonDarkholme Creo que sería un enfoque muy pragmático que proporcionaría la mayor parte de la expresividad deseada. ¡Gran idea!

Definitivamente quiero ver esto algún día.

A menudo escribo una clase para Angular 2 o para Aurelia, que se ve así:

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

Lo que quiero escribir es algo como
decoradores/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;
    }
  }
}

y luego podría aplicarlo genéricamente como

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

sin pérdida de seguridad tipográfica. Esto sería una gran ayuda para escribir código limpio y expresivo.

Reviviendo este tema.

Ahora que # 13743 está disponible y el soporte de mezcla está en el idioma, esta es una función súper útil.

Sin embargo, @HerringtonDarkholme es menos adecuado para este caso, tener que declarar el tipo de retorno del decorador pierde algunas características dinámicas...

@ahejlsberg , @mhegazy ¿Crees que esto es factible?

Tengo otro escenario de uso que no estoy seguro de que esté cubierto por esta conversación, pero probablemente caiga bajo el mismo paraguas.

Me gustaría implementar un decorador de métodos que cambie el tipo del método por completo (no el tipo de retorno o los parámetros, sino la función completa). p.ej

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

Totalmente posible en JavaScript, obviamente ese no es el problema, pero en TypeScript necesito el decorador asyncTask para cambiar el tipo del método decorado de () => Promise<void> a AsyncTask<() => Promise<void>> .

¿Está bastante seguro de que esto no es posible ahora y probablemente cae bajo el paraguas de este problema?

@codeandcats ¡ su ejemplo es exactamente el mismo caso de uso por el que estoy aquí!

Hola @ohjames , perdóname, tengo problemas para asimilar tu ejemplo, ¿hay alguna posibilidad de que puedas reescribirlo en algo que funcione como está en el patio de recreo?

¿Algún progreso en esto? Tuve esto en mi cabeza todo el día, sin darme cuenta de este problema, fui a implementarlo solo para descubrir que el compilador no lo detecta. Tengo un proyecto que podría usar una mejor solución de registro, así que escribí un singleton rápido para luego expandirlo a un registrador completo que iba a adjuntar a las clases a través de un decorador como

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

y escribí el código necesario para ello

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

y la propiedad logger definitivamente está presente en el tiempo de ejecución pero, lamentablemente, el compilador no la detecta.

Me encantaría ver alguna solución a este problema, especialmente porque una construcción en tiempo de ejecución como esta debería poder representarse correctamente en tiempo de compilación.

Terminé conformándome con un decorador de propiedades solo para ayudarme por ahora:

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

y adjuntarlo a clases como

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

pero esto es mucho más repetitivo por clase utilizando el registrador que un simple decorador de clase @loggable . Supongo que podría encasillar como (this as Loggable<this>).logger pero esto también está bastante lejos de ser ideal, especialmente después de hacerlo un puñado de veces. Se cansaría muy rápido.

Tuve que usar TS para una aplicación completa principalmente porque no pude hacer que https://github.com/jeffijoe/mobx-task trabajara con decoradores. Espero que esto se aborde pronto. 😄

Es muy irritante en el ecosistema Angular 2 donde los decoradores y TypeScript son tratados como ciudadanos de primera clase. Sin embargo, en el momento en que intenta agregar una propiedad con un decorador, el compilador de TypeScript dice que no. Hubiera pensado que el equipo de Angular 2 mostraría algún interés en este problema.

@zajrik puede lograr lo que quiera con los mixins de clase que han sido compatibles con la tipificación adecuada desde TS 2.2:

Defina su mezcla Loggable así:

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

y luego puedes usarlo de varias maneras. Ya sea en la cláusula extends de una declaración de clase:

class Foo {
  superProperty: string;
}

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

TS sabe que las instancias de LoggableFoo tienen superProperty , logger y subProperty :

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

También puede usar un mixin como una expresión que devuelve la clase concreta que desea usar:

const LoggableFoo = Loggable(Foo);

También _puedes_ usar una mezcla de clase como decorador, pero tiene una semántica ligeramente diferente, principalmente que es una subclase de tu clase, en lugar de permitir que tu clase la subclasifique.

Los mixins de clase tienen varias ventajas sobre los decoradores, en mi opinión:

  1. Crean una nueva superclase, de modo que la clase a la que los aplica tiene un cambio para anularlos
  2. Escriben ahora, sin ninguna característica adicional de TypeScript
  3. Funcionan bien con la inferencia de tipos: no tiene que escribir el valor de retorno de la función mixin
  4. Funcionan bien con el análisis estático, especialmente con el salto a la definición. Saltar a la implementación de logger lo lleva a la _implementación_ de la mezcla, no a la interfaz.

@justinfagnani Ni siquiera había considerado mixins para esto, así que gracias. Continuaré y escribiré una combinación de Loggable esta noche para hacer que la sintaxis de mi archivo adjunto Logger sea un poco más agradable. La ruta extends Mixin(SuperClass) es mi preferida ya que es la forma en que he usado mixins hasta ahora desde el lanzamiento de TS 2.2.

Sin embargo, prefiero la idea de la sintaxis del decorador a los mixins, por lo que todavía espero que se pueda encontrar alguna solución para este problema específico. Ser capaz de crear mixins sin repeticiones utilizando decoradores sería una gran ayuda para un código más limpio, en mi opinión.

@zajrik me alegro de que la sugerencia haya ayudado, espero

Todavía no entiendo muy bien cómo los mixins tienen más repeticiones que los decoradores. Son casi idénticos en peso sintáctico:

Mezcla de clase:

class LoggableFoo extends Loggable(Foo) {}

vs decorador:

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

En mi opinión, el mixin es mucho más claro acerca de su intención: está generando una superclase, y las superclases definen a los miembros de una clase, por lo que probablemente el mixin también esté definiendo miembros.

Los decoradores se utilizarán para tantas cosas que no se puede asumir si son o no miembros definitorios. Podría ser simplemente registrar la clase para algo o asociarle algunos metadatos.

Para ser justos, creo que lo que quiere @zajrik es:

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

Lo cual es innegablemente, aunque sea un poco, menos repetitivo.

Dicho esto, me encanta la solución mixin. Sigo olvidando que los mixins son una cosa.

Si todo lo que le importa es agregar propiedades a la clase actual, entonces los mixins son básicamente equivalentes a los decoradores con una molestia importante... si su clase aún no tiene una superclase, necesita crear una superclase vacía para usarlas. También la sintaxis parece más pesada en general. Además, no está claro si se admiten mixins paramétricos (se permiten extends Mixin(Class, { ... }) ).

@justinfagnani en su lista de razones, los puntos 2-4 son en realidad deficiencias en TypeScript, no ventajas de mixins. No se aplican en un mundo JS.

Creo que todos deberíamos tener claro que una solución basada en mixin para el problema de los OP implicaría agregar dos clases a la cadena de prototipos, una de las cuales es inútil. Sin embargo, esto refleja las diferencias semánticas de los decoradores Vs mixins, los mixins le dan la oportunidad de interceptar la cadena de la clase principal. Sin embargo, el 95% de las veces esto no es lo que la gente quiere hacer, quieren decorar esta clase. Si bien los mixins tienen usos limitados, creo que promocionarlos como una alternativa a los decoradores y las clases de orden superior es semánticamente inapropiado.

Los mixins son básicamente equivalentes a los decoradores con una molestia importante... si su clase aún no tiene una superclase, necesita crear una superclase vacía para usarla.

Esto no es necesariamente cierto:

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

class Foo extends Mixin() {}

También la sintaxis parece más pesada en general.

Simplemente no veo cómo es esto así, así que tendremos que estar en desacuerdo.

Además, no está claro si se admiten mixins paramétricos (se extiende Mixin (Class, { ... }) permitido).

Lo son mucho. Los mixins son solo funciones.

en su lista de razones, los puntos 2-4 son en realidad deficiencias en TypeScript, no ventajas de mixins. No se aplican en un mundo JS.

Este es un problema de TypeScript, por lo que se aplican aquí. En el mundo JS, los decoradores aún no existen.

Creo que todos deberíamos tener claro que una solución basada en mixin para el problema de los OP implicaría agregar dos clases a la cadena de prototipos, una de las cuales es inútil.

No tengo claro de dónde sacas dos. Es uno, al igual que lo haría el decorador, a menos que esté parcheando. ¿Y qué prototipo es inútil? La aplicación mixin presumiblemente agrega una propiedad/método, eso no es inútil.

Sin embargo, esto refleja las diferencias semánticas de los decoradores Vs mixins, los mixins le dan la oportunidad de interceptar la cadena de la clase principal. Sin embargo, el 95% de las veces esto no es lo que la gente quiere hacer, quieren decorar esta clase.

No estoy tan seguro de que esto sea cierto. Por lo general, al definir una clase, espera que esté en la parte inferior de la jerarquía de herencia, con la capacidad de anular los métodos de la superclase. Los decoradores tienen que parchear la clase, que tiene numerosos problemas, como no trabajar con super() , o extenderla, en cuyo caso la clase decorada no tiene la capacidad de anular la extensión. Esto puede ser útil en algunos casos, como un decorador que anula todos los métodos definidos de la clase para el seguimiento de rendimiento/depuración, pero está lejos del modelo de herencia habitual.

Si bien los mixins tienen usos limitados, creo que promocionarlos como una alternativa a los decoradores y las clases de orden superior es semánticamente inapropiado.

Cuando un desarrollador quiere agregar miembros a la cadena de prototipos, los mixins son exactamente semánticamente apropiados. En todos los casos en los que he visto a alguien querer usar decoradores para mixins, usar mixins de clase lograría la misma tarea, con la semántica que en realidad esperan de los decoradores, más flexibilidad debido a la propiedad de trabajo con superllamadas, y de Por supuesto que funcionan ahora.

Los mixins difícilmente son inapropiados cuando abordan directamente el caso de uso.

Cuando un desarrollador quiere agregar miembros a la cadena de prototipos

Ese es exactamente mi punto, el OP no quiere agregar nada a la cadena de prototipos. Solo quiere mutar una sola clase, y la mayoría de las veces, cuando las personas usan decoradores, ni siquiera tienen una clase principal que no sea Object. Y por alguna razón Mixin(Object) no funciona en TypeScript, por lo que debe agregar una clase vacía ficticia. Así que ahora tiene una cadena de prototipos de 2 (sin incluir Objeto) cuando no la necesita. Además, hay un costo no trivial para agregar nuevas clases a la cadena de prototipos.

En cuanto a la sintaxis compare Mixin1(Mixin2(Mixin3(Object, { ... }), {... }), {...}) . Los parámetros para cada mixin están tan lejos de la clase mixin como podrían estar. La sintaxis del decorador es claramente más legible.

Si bien la sintaxis del decorador en sí no verifica el tipo, puede usar la invocación de funciones regulares para obtener lo que desea:

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

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

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

Es un poco molesto que tengas que declarar tu clase como const Foo = loggable(class { , pero aparte de eso, todo funciona.

@ohjames (cc @justinfagnani) debe tener cuidado al extender componentes como Object (ya que atacan el prototipo de su subclase en instancias): https://github.com/Microsoft/TypeScript/wiki/FAQ #por qué -no-extienden-los-incorporados-como-la-matriz-de-errores-y-el-mapa-funciona

@nevir sí, ya probé la sugerencia de @justinfagnani de usar un mixin con un parámetro Object predeterminado en el pasado con TypeScript 2.2 y tsc rechaza el código.

@ohjames todavía funciona, solo debe tener cuidado con el prototipo en el caso predeterminado (consulte la entrada de preguntas frecuentes).

Sin embargo, generalmente es más fácil confiar en el comportamiento de tslib.__extend cuando pasa null

¿Algún plan para enfocar este problema en el siguiente paso de la iteración? Los beneficios de esta función son extremadamente altos en tantas bibliotecas.

Acabo de encontrarme con este problema: me obliga a escribir una gran cantidad de código innecesario. Resolver este problema sería de gran ayuda para cualquier marco/biblioteca basado en decoradores.

@TomMarius Como mencioné anteriormente, las clases envueltas en funciones de decorador ya escriben correctamente, simplemente no puede usar el azúcar de sintaxis @ . En lugar de hacer:

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

solo necesitas hacer:

const Foo = loggable(class { });

Incluso puede componer un montón de funciones de decorador juntas antes de envolver una clase en ellas. Si bien hacer que el azúcar de sintaxis funcione correctamente es valioso, no parece que esto deba ser un punto de dolor tan grande como lo son las cosas.

@masaeedu Realmente, el problema no es el soporte de tipo externo sino interno. Poder usar las propiedades que agrega el decorador dentro de la propia clase sin errores de compilación es el resultado deseado, al menos para mí. El ejemplo que proporcionó solo le daría a Foo el tipo registrable pero no permitiría el tipo para la definición de clase en sí.

@zajrik Un decorador devuelve una nueva clase de una clase original, incluso cuando usa la sintaxis incorporada @ . Obviamente, JS no impone la pureza, por lo que puede mutar la clase original que se le pasa, pero esto es incongruente con el uso idiomático del concepto de decorador. Si está acoplando estrechamente la funcionalidad que está agregando a través de los decoradores a las funciones internas de la clase, también pueden ser propiedades internas.

¿Puede darme un ejemplo de un caso de uso para una API de consumo interno de clase que se agrega en algún momento posterior a través de decoradores?

El ejemplo de Logger anterior es un buen ejemplo de un _deseo_ común para poder manipular las partes internas de la clase decorada. (Y es familiar para las personas que vienen de otros idiomas con decoración de clase , como Python )

Dicho esto, la sugerencia de mezcla de clases de @justinfagnani parece una buena alternativa para ese caso.

Si desea poder definir las partes internas de una clase, la forma estructurada de hacerlo no es parchear la clase o definir una nueva subclase, ya que TypeScript tendrá dificultades para razonar en el contexto de la clase. en sí mismo, sino simplemente definir cosas en la clase en sí, o crear una nueva superclase que tenga las propiedades necesarias, sobre las cuales TypeScript puede razonar.

Los decoradores realmente no deberían cambiar la forma de una clase de manera que sea visible para la clase o la mayoría de los consumidores. @masaeedu está justo aquí.

Si bien lo que dice es cierto, TypeScript no está ahí para imponer prácticas de codificación limpias, sino para escribir correctamente el código JavaScript, y falla en este caso.

@masaeedu Lo que dijo @zajrik . Tengo un decorador que declara un "servicio en línea" que agrega un montón de propiedades a una clase que luego se usan en la clase. La creación de subclases o la implementación de una interfaz no es una opción debido a la falta de metadatos y la aplicación de restricciones (si se esfuerza por no duplicar el código).

@TomMarius Mi punto es que es una verificación de tipo correcta. Cuando aplica una función de decorador a una clase, la clase no cambia de ninguna manera. Se produce una nueva clase a través de alguna transformación de la clase original, y solo se garantiza que esta nueva clase admitirá la API introducida por la función de decorador.

No sé qué significa "metadatos faltantes y aplicación de restricciones" (tal vez un ejemplo concreto ayudaría), pero si su clase se basa explícitamente en la API introducida por el decorador, debería subclasificarla directamente a través del patrón de mezcla que mostró @justinfagnani , o inyectarlo a través del constructor o algo así. La utilidad de los decoradores es que permiten que las clases que están cerradas a la modificación se extiendan en beneficio del código que consume esas clases . Si tiene la libertad de definir la clase usted mismo, simplemente use extends .

@masaeedu Si está desarrollando algún tipo de, digamos, una biblioteca RPC, y desea obligar al usuario a escribir solo métodos asincrónicos, el enfoque basado en la herencia lo obliga a duplicar el código (o no he encontrado la manera correcta , tal vez, me alegraría que me dijeras si sabes cómo).

Enfoque basado en la herencia
Definición: export abstract class Service<T extends { [P in keyof T]: () => Promise<IResult>}> { protected someMethod(): Promise<void> { return Promise.reject(""); } }
Uso: export default class MyService extends Service<MyService> { async foo() { return this.someMethod(); } }

Enfoque basado en el decorador
Definición: export function service<T>(target: { new (): T & { [P in keyof T]: () => Promise<IResult> } }) { target.someMethod = function () { return Promise.reject(""); }; return target; }
Uso: <strong i="17">@service</strong> export default class { async foo() { return this.someMethod(); } }

Puede ver claramente la duplicación de código en el ejemplo de enfoque basado en la herencia. A mí y a mis usuarios nos ha pasado muchas veces que se olvidaron de cambiar el parámetro de tipo cuando copiaron y pegaron la clase o comenzaron a usar "cualquiera" como parámetro de tipo y la biblioteca dejó de funcionar para ellos; el enfoque basado en el decorador es mucho más amigable para los desarrolladores.

Después de eso, hay otro problema con el enfoque basado en la herencia: ahora faltan los metadatos de reflexión, por lo que debe duplicar el código aún más porque debe presentar el decorador service todos modos. El uso ahora es: <strong i="22">@service</strong> export default class MyService extends Service<MyService> { async foo() { return this.someMethod(); } } , y eso es simplemente hostil, no solo un pequeño inconveniente.

Es cierto que, semánticamente, la modificación se realiza después de definir la clase; sin embargo, no hay forma de instanciar la clase no decorada, por lo que no hay razón para no admitir correctamente la mutación de la clase, aparte de que a veces permite código sucio (pero a veces para mejor bien). Recuerde que JavaScript todavía se basa en prototipos, la sintaxis de la clase es solo un azúcar para cubrirlo. El prototipo es mutable y el decorador puede silenciarlo, y debe escribirse correctamente.

Cuando aplica una función de decorador a una clase, la clase no cambia de ninguna manera.

No es cierto, cuando aplica una función de decorador a una clase, la clase puede cambiarse de cualquier manera. Te guste o no.

@TomMarius Está tratando de explotar la inferencia para hacer cumplir algún contrato, lo cual es totalmente irrelevante para el argumento que se tiene aquí. Solo debes hacer:

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

No hay absolutamente ningún requisito para que los internos de la clase estén al tanto de la función de decoración.

@masaeedu Ese no era mi punto. El decorador de servicios/clase de servicio introduce algunas propiedades nuevas, y estas siempre están disponibles para la clase que se utilizará, pero el sistema de tipos no lo refleja correctamente. Dijiste que debería usar la herencia para eso, así que te mostré por qué no puedo/no quiero hacerlo.

He editado el ejemplo para que quede más claro.

@masaeedu Por cierto, la afirmación "Un decorador devuelve una nueva clase de una clase original" es incorrecta: todos los decoradores que hemos mostrado aquí devuelven una clase mutada o directamente la original, nunca una nueva.

@TomMarius Su comentario mencionó un problema con "obligar al usuario a escribir solo métodos asincrónicos", que es el problema que traté de abordar en mi comentario. Hacer cumplir que el usuario ha seguido el contrato que espera debe hacerse siempre que el código del usuario se devuelva a la biblioteca, y no tiene nada que ver con la discusión sobre si los decoradores deben cambiar la forma de tipo presentada a las partes internas de la clase. El problema ortogonal de proporcionar una API al código del usuario se puede resolver con enfoques de composición o herencia estándar.

@ohjames La clase no se cambia simplemente aplicando un decorador. JS no impone la pureza, por lo que, obviamente, cualquier declaración en cualquier parte de su código puede modificar cualquier otra cosa, pero esto es irrelevante para esta discusión de funciones. Incluso una vez que se implemente la función, TypeScript no lo ayudará a realizar un seguimiento de los cambios estructurales arbitrarios dentro de los cuerpos de las funciones.

@masaeedu Estás eligiendo partes, pero estoy hablando del panorama general. Revise todos mis comentarios en este hilo: el punto no está en los problemas individuales sino en que cada problema ocurra al mismo tiempo. Creo que expliqué bien el problema con el enfoque basado en la herencia: mucha, mucha duplicación de código.

Para mayor claridad, esto no se trata de "código limpio" para mí. El problema es de practicidad; no necesita cambios masivos en el sistema de tipos si trata @foo igual que la aplicación de función. Si abre la lata de gusanos para tratar de introducir información de tipo en el argumento de una función desde su tipo de retorno , mientras que al mismo tiempo interactúa con la inferencia de tipo y todas las otras bestias mágicas que se encuentran en varios rincones del sistema de tipo TypeScript, siento esto se convertirá en un gran obstáculo para las nuevas funciones de la misma manera que lo es ahora la sobrecarga.

@TomMarius Su primer comentario en este hilo es sobre código limpio, que no es relevante. El siguiente comentario es sobre este concepto de servicio en línea para el que proporcionó el código de ejemplo. La queja principal, desde el primer párrafo hasta el cuarto, se trata de cómo es propenso a errores usar MyService extends Service<MyService> . Traté de mostrar un ejemplo de cómo puedes lidiar con esto.

Lo miré de nuevo, y realmente no puedo ver nada en ese ejemplo que ilustre por qué los miembros de la clase decorada necesitarían estar al tanto del decorador. ¿Qué tienen estas nuevas propiedades que proporciona al usuario que no se pueden lograr con la herencia estándar? No he trabajado con la reflexión, así que lo pasé por alto, mis disculpas.

@masaeedu Puedo lograrlo con la herencia, pero la herencia me obliga a mí o a mis usuarios a duplicar masivamente el código, por lo que me gustaría tener otra forma, y ​​lo hago, pero el sistema de tipos no puede reflejar correctamente la realidad.

El punto es que el tipo correcto de <strong i="7">@service</strong> class X { } donde service se declara como <T>(target: T) => T & IService no es X , sino X & IService ; y el problema es que en realidad es cierto incluso dentro de la clase, aunque semánticamente no lo sea.

Otro gran problema que causa este problema es que cuando tienes una serie de decoradores, cada uno con algunas restricciones, el sistema de tipos piensa que el objetivo es siempre la clase original, no la decorada, y por lo tanto la restricción es inútil.

Puedo lograrlo con la herencia, pero la herencia me obliga a mí o a mis usuarios a duplicar masivamente el código, por lo que me gustaría tener otra forma.

Esta es la parte que no estoy entendiendo. Sus usuarios necesitan implementar IService , y quiere asegurarse de que TheirService implements IService también obedezca algún otro contrato { [P in keyof blablablah] } , y quizás también quiera que tengan un { potato: Potato } en su servicio. Todo esto es fácil de lograr sin que los miembros de la clase se den cuenta 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 ""; }
}

En ninguno de los dos casos hay una ganancia enorme en concisión que solo se puede lograr presentando el tipo de retorno de serviceDecorator como el tipo de this a foo . Más importante aún, ¿cómo propones idear una estrategia de tipeo uniforme para esto? El tipo de devolución de serviceDecorator se deduce en función del tipo de clase que está decorando, que a su vez ahora se escribe como el tipo de devolución del decorador...

Hola @masaeedu , la brevedad se vuelve especialmente valiosa cuando tienes más de uno.

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

Sin embargo, esta solución es solo una alternativa a los decoradores de clase. Para los decoradores de métodos, actualmente no hay soluciones y bloquea tantas implementaciones agradables. Los decoradores están en propuesta en la etapa 2: https://github.com/tc39/proposal-decorators . Sin embargo, hubo muchos casos en que la implementación se hizo mucho antes. Creo que especialmente los decoradores son uno de esos ladrillos importantes que fueron realmente importantes ya que ya se usaban en muchos marcos y ya se implementó una versión muy simple en babel/ts. Si este problema pudiera implementarse, no perdería su estado experimental hasta el lanzamiento oficial. Pero eso es "experimental" para.

@pietschy Sí, sería bueno hacer que el azúcar de sintaxis @ funcione correctamente con la verificación de tipos. Por el momento, puede usar la composición de funciones para obtener una concisión razonablemente similar:

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

const MyComponent = decorator(class {
});

La discusión anterior trata sobre si es una buena idea hacer algún tipo de escritura inversa en la que el tipo de retorno del decorador se presente como el tipo de this a los miembros de la clase.

Hola @masaeedu , sí, entendí el contexto de la discusión, de ahí el // do stuff with undo context, component methods etc . Salud

Realmente necesito hacer que Mixins sea más fácil.

mecanografiado (javascript) no admite herencia múltiple, por lo que debemos usar Mixins o Traits.
Y ahora me está haciendo perder tanto tiempo, especialmente cuando reconstruyo algo.
Y debo copiar y pegar una interfaz con su "implementación vacía" en todas partes. (#371)

--
No creo que el tipo de retorno del decorador deba presentarse en la clase.
porque: .... emmm. no sé cómo describirlo, lo siento por mi pobre habilidad en inglés ... ( 🤔 ¿puede existir una foto sin un marco de fotos?) este trabajo es para un interface .

¡Voy a agregar mi +1 a este! Me encantaría verlo venir pronto.

@pietschy Si la clase depende de los miembros agregados por los decoradores, entonces debería extender el resultado de los decoradores, no al revés. Deberías hacer:

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

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

La alternativa es que el sistema de tipos funcione en algún tipo de ciclo, donde el argumento de la función decoradora se escribe contextualmente por su tipo de retorno, que se infiere de su argumento, que se escribe contextualmente por su tipo de retorno, etc. Todavía necesitamos un propuesta específica sobre cómo debería funcionar esto, no es solo esperar a que se implemente.

Hola, @masaeedu , no sé por qué tendría que componer mis decoradores y aplicarlos a una clase base. Según tengo entendido, el prototipo ya se ha modificado cuando se ejecuta cualquier código de tierra del usuario. P.ej

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

Entonces, si los métodos definidos por los decoradores ya existen y pueden llamarse legítimamente, sería bueno si el mecanografiado reflejara esto.

El deseo es que el mecanografiado refleje esto sin imponer soluciones alternativas. si hay otros
cosas que pueden hacer los decoradores que no se pueden modelar, al menos sería bueno si esto
escenario fue apoyado de alguna forma o forma.

Este hilo está lleno de opiniones sobre lo que deben hacer los decoradores, cómo deben usarse, cómo no deben usarse, etc. hasta la saciedad .

Esto es lo que realmente hacen los decoradores:

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

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

lo que se convierte, si apuntas a 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);

con __decorate aplicando cada decorador de abajo hacia arriba, y cada uno tiene la opción de devolver una clase completamente nueva.

Entiendo que hacer que el sistema de tipos admita y reconozca esto puede ser complicado, y entiendo que puede llevar tiempo admitir esto, pero ¿no podemos todos estar de acuerdo en que el comportamiento actual, del ejemplo de código original anterior que inició esto? hilo, es simplemente incorrecto?

No importa cuál sea la opinión de cualquier desarrollador sobre decoradores o mezclas o composición funcional, etc., etc., esto parece ser un error bastante claro.


¿Puedo preguntar cortésmente si esto está en preparación para una versión futura?

TypeScript es simplemente increíble y me encanta; Sin embargo, esto parece una de las pocas (¿únicas?) piezas que simplemente están rotas, y estoy ansioso por ver cómo se arreglan :)

@arackaf Se entiende bien lo que realmente hacen los decoradores, y suponiendo que no le importen los tipos que emite TypeScript ya es compatible con los decoradores. Todo el debate en este número es sobre cómo deben presentarse las clases decoradas en el sistema de tipos.

Estoy de acuerdo en que new Foo().foo es un error de tipo y es un error que se soluciona fácilmente. No estoy de acuerdo en que return this.foo; sea ​​un error de tipo sea un error. En todo caso, está solicitando una función en el sistema de tipos que hasta ahora nadie en este problema ha especificado. Si tiene algún mecanismo en mente por el cual el tipo de this debe ser transformado por un decorador aplicado a la clase contenedora, debe sugerir este mecanismo explícitamente .

Tal mecanismo no es tan trivial como cabría esperar, porque en casi todos los casos el tipo de retorno del decorador se deduce del mismo tipo que propone transformar utilizando el tipo de retorno del decorador. Si el decorador addMethod toma una clase de tipo new () => T y produce new() => T & { hello(): void } , no tiene sentido sugerir que T debería ser T & { hello(): void } . Dentro del cuerpo del decorador, ¿es válido para mí referirme a super.hello ?

Esto es especialmente pertinente porque no estoy restringido a hacer return class extends ClassIWasPassed { ... } en el cuerpo del decorador, puedo hacer lo que me plazca; tipos de sustracción, tipos mapeados, uniones, todos son juegos limpios. Cualquier tipo resultante debe funcionar bien con este ciclo de inferencia que está sugiriendo. Como ilustración del problema, dígame qué debería suceder en este ejemplo:

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

No puede simplemente descartar el problema como "complicado", alguien debe proponer cómo debería funcionar esto.

No es un error: está pasando a Logger una clase que se ajusta a Greeter; sin embargo, el decorador muta a Foo para que sea algo completamente diferente, que ya no se extiende a Greeter. Por lo tanto, debería ser válido para el decorador del registrador tratar a Foo como Greeter, pero no válido para cualquier otra persona, ya que el decorador reasigna a Foo a algo completamente diferente.

Estoy seguro de que implementar esto sería muy difícil. ¿Es posible algún subconjunto razonable, para los casos comunes como el que se enumera en la parte superior de este hilo, con soluciones alternativas como la fusión de interfaces como respaldo para casos complicados como este?

@arackaf

sin embargo, el decorador muta a Foo para que sea algo completamente diferente.

No existe una definición precisa de lo que es Foo en primer lugar. Recuerde que todo nuestro punto de discordia es si los miembros Foo deberían poder acceder (y, por lo tanto, regresar como parte de la API pública), los miembros presentados por el decorador de this . Lo que es Foo se define por lo que devuelve el decorador, y lo que devuelve el decorador se define por lo que es Foo . Están inseparablemente unidos.

Estoy seguro de que implementar esto sería brutalmente difícil.

Todavía es prematuro hablar sobre la complejidad de la implementación, cuando ni siquiera tenemos una propuesta sólida sobre cómo debería funcionar la función. Disculpas por insistir en esto, pero realmente necesitamos que alguien proponga un mecanismo concreto para "lo que sucede con this cuando aplico tal secuencia de decoradores a tal clase". Luego podemos conectar varios conceptos de TypeScript en diferentes partes y ver si lo que sale tiene sentido. Solo entonces tiene sentido discutir la complejidad de la implementación.

¿La salida del decorador actual que pegué arriba no cumple con las especificaciones? Supuse que era por lo que he visto.

Suponiendo que lo sea, Foo tiene un significado bastante preciso en cada paso del camino. Espero que TS me permita usar this (métodos dentro de Foo y en instancias de Foo) según lo que devuelva el último decorador, y TS me obligará a agregar anotaciones de tipo en el camino según sea necesario si el decorador funciones no son suficientemente analizables.

@arackaf No hay "pasos del camino"; solo nos preocupa el tipo final de this para un fragmento de código inmutable dado. Debe describir, al menos en un nivel alto, lo que espera que sea el tipo de this para los miembros de una clase X decorada con funciones de decorador f1...fn , en términos de las firmas tipo de X y f1...fn . Puede tener tantos pasos en el proceso como desee. Hasta ahora, nadie ha estado haciendo esto. He estado suponiendo que la gente quiere decir que el tipo de devolución del decorador debe presentarse como el tipo de this , pero por lo que sé, podría estar totalmente equivocado.

Si su propuesta es hacer que los tipos reflejen mecánicamente lo que sucede con los valores en la salida transpilada, terminará con lo que estoy proponiendo en lugar de lo que está proponiendo: es decir new Foo().hello() está bien, pero this.hello() no lo es. En ningún momento de ese ejemplo, la clase original que está decorando obtiene un método hello . Solo el resultado de __decorate([addMethod], Foo) (que luego se asigna a Foo ) tiene un método hello .

He estado suponiendo que la gente quiere decir que el tipo de retorno del decorador debe presentarse como el tipo de este

Oh, lo siento, sí, así es. Exactamente eso. Punto final. Porque eso es lo que HACEN los decoradores. Si el último decorador de la fila devuelve una clase completamente nueva, entonces eso ES lo que es Foo.

En otras palabras:

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

Foo es lo que en el mundo c dice que es. Si c es una función que devuelve any entonces, no lo sé, ¿tal vez solo vuelva al Foo original? Eso parece un enfoque razonable y compatible con versiones anteriores.

pero si c devuelve algún tipo nuevo X , entonces espero absolutamente que Foo respete eso.

¿Me estoy perdiendo de algo?


aclarando más, 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 

¿Me estoy perdiendo de algo?

@arackaf Sí, lo que le falta a este enfoque ingenuo es que un decorador es libre de devolver tipos arbitrarios, sin restricción de que el resultado sea compatible con el tipo de Foo .

Hay una gran cantidad de absurdos que puedes producir con esto. Digamos que suspendo mi objeción sobre la circularidad de escribir this como resultado de c , que está determinado por el tipo de this , que está determinado por el resultado de c , etc. Esto sigue sin funcionar. Aquí hay otro ejemplo sin sentido:

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

En este caso, se ve obligado a generar un error para un código totalmente benigno o ajustar la proposición de que this sea ​​exactamente idéntico al tipo de retorno del decorador.

No estoy seguro de ver el problema. @logger ha elegido devolver una clase completamente nueva, sin ningún método foo y con un método hello() totalmente nuevo, que hace referencia al original, ahora inalcanzable Foo .

new Foo().foo()

ya no es válido; producirá un error de tiempo de ejecución. Solo digo que también debería producir un error en tiempo de compilación.

Dicho esto, si analizar estáticamente todo eso es demasiado difícil, sería totalmente razonable en mi opinión obligarnos a agregar anotaciones de tipo explícito a logger para indicar exactamente lo que se devuelve. Y si no hay tal anotación de tipo presente, entonces diría que vuelva a asumir que se devuelve Foo . Eso debería mantenerlo compatible con versiones anteriores.

@arackaf No hay problema con el código en términos de escritura o evaluación del tiempo de ejecución. Puedo llamar a new Foo().hello() , que internamente llamará a hello de la clase decorada, que llamará a bar de la clase decorada. No es un error para mí invocar bar dentro de la clase original.

Puede probarlo usted mismo ejecutando este ejemplo completo en el patio de recreo:

// Code from previous snippet...

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

Claro, pero dije que fue un error invocar

new Foo().foo()

No hay ningún problema con el código en términos de escritura o evaluación del tiempo de ejecución. Puedo llamar a new Foo().hello(), que internamente llamará al saludo de la clase decorada, que llamará a la barra de la clase decorada

Pero debería ser un error decir

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

ya que el método hello de Foo ahora devuelve void, según la clase que devuelve Logger.

Claro, pero dije que fue un error invocar new Foo().foo()

@arackaf Pero eso no importa. No invoqué new Foo().foo() . Invoqué this.foo() y obtuve un error de tipo, aunque mi código funciona bien en tiempo de ejecución.

Pero debería ser un error decir let s : string = new Foo().hello()

Una vez más, esto es irrelevante. No digo que el tipo final de Foo.prototype.hello deba ser () => string (estoy de acuerdo en que debería ser () => void ). Me quejo de que la invocación válida this.bar() se haya errado, porque ha trasplantado quirúrgicamente un tipo en el que no tiene sentido trasplantarlo.

Hay dos Foo aquí. Cuando tu dices

class Foo { 
}

Foo es un enlace inmutable DENTRO de la clase y un enlace mutable fuera de la clase. Entonces esto funciona perfectamente como puedes verificar en un jsbin

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

let F = Foo;

Foo = null;

new F().hello();

Su ejemplo anterior hace cosas similares; captura una referencia a la clase original antes de que se mute ese enlace externo. Todavía no estoy seguro de a qué te diriges.

this.foo(); es perfectamente válido, y no esperaría un error de tipo (tampoco culparía a la gente de TS si necesitara alguna referencia, ya que estoy seguro de que sería difícil rastrear esto)

this.foo(); es perfectamente válido y no esperaría un error de tipo

Bien, estamos de acuerdo, pero esto significa que ahora debe calificar o descartar la propuesta de que this sea ​​el tipo de instancia de lo que devuelva el decorador. Si cree que no debería ser un error de tipo, ¿qué debería ser this en lugar de { hello(): void } en mi ejemplo?

this depende de lo que se instancia.

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

¿Podríamos ceñirnos a un ejemplo concreto? Eso haría las cosas mucho más fáciles de entender para mí. En el siguiente fragmento:

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

¿Cuál es el tipo de this ? Si es { hello(): void } , obtendré un error de tipo, porque foo no es miembro de { hello(): void } . Si no es { hello(): void } , entonces this no es simplemente el tipo de instancia del tipo de devolución del decorador, y debe explicar cualquier lógica alternativa que esté usando para llegar al tipo de this .

EDITAR: Olvidé agregar el decorador en Foo . Reparado.

this , donde tiene las flechas, es, por supuesto, una instancia del Foo original. No hay error de tipo.

Ah, ahora veo tu punto; pero sigo sin ver donde esta el problema. this.foo() DENTRO del Foo original no es un error de tipo, eso es válido para la clase (ahora inalcanzable) que solía estar vinculada al identificador Foo .

Es una idiosincrasia, una trivia divertida, pero no veo por qué esto debería evitar que TS maneje decoradores de clase mutantes.

@arackaf No estás respondiendo la pregunta. ¿Cuál es, específicamente, el tipo de this ? No puede simplemente responder circularmente sin fin " this es Foo y Foo es this ". ¿Qué miembros tiene this ? Si tiene miembros además hello(): void , ¿qué lógica se usa para determinarlos?

Cuando dices " this.foo() DENTRO del Foo original no es un error de tipo", aún debes responder a la pregunta: ¿cuál es el tipo estructural de this tal que no es un error de tipo para hacer this.foo() ?

Además, la clase original no es "inalcanzable". Cada función definida en ese fragmento de código se ejerce activamente en tiempo de ejecución, y todo funciona sin problemas. Ejecute el enlace del patio de recreo que proporcioné y mire la consola. El decorador devuelve una nueva clase en la que el método hello delega en el método hello de la clase decorada, que a su vez delega en el método foo de la clase decorada.

Es una idiosincrasia, una trivia divertida, pero no veo por qué esto debería evitar que TS maneje decoradores de clase mutantes.

No hay "trivia" en el sistema de tipos. No obtendrá un error de tipo TSC-1234 "naughty boy, you can't do that" porque el caso de uso es demasiado específico. Si una característica está causando que un código perfectamente normal se rompa de manera sorprendente, la característica debe repensarse.

No obtendrá un error de tipo TSC-1234 "naughty boy, you can't do that" porque el caso de uso es demasiado específico.

Eso es EXACTAMENTE lo que obtengo cuando trato de usar un método que un decorador agregó a una definición de clase. Actualmente tengo que solucionarlo agregando una definición a la clase o usando la combinación de interfaces, emitiendo como any etc.

He respondido todas las preguntas sobre qué es this y dónde.

El simple hecho del asunto es que el significado de this cambia según el lugar donde se encuentre.

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"

cuando dices new Class() - Class apunta al Foo original, y desde la perspectiva de TypeScript tendría acceso a hello(): string ya que así es como se escribe Class (extiende a Greeter). En el tiempo de ejecución, creará una instancia del Foo original.

new LoggableFoo().hello() llama a un método void que llama a un método que de otro modo sería inalcanzable, escrito a través de Greeter.

si lo hubieras hecho

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

entonces Foo ahora es una clase con solo hello(): void y new Foo().foo() debería ser un error de tipo.

Y, nuevamente, hello() { return this.foo(); } no es un TypeError, ¿por qué sería? El hecho de que ya no se pueda acceder a esa definición de clase no hace que ese método sea inválido.

No espero que TypeScript obtenga ninguno de estos casos extremos perfectos; sería bastante comprensible tener que agregar anotaciones de tipo aquí y allá, como siempre. Pero ninguno de estos ejemplos muestra por qué @logger no puede cambiar fundamentalmente a lo que está vinculado Foo .

Si logger es una función que devuelve una nueva clase, entonces ESO es a lo que ahora hace referencia Foo.

He respondido todas las preguntas sobre qué es esto y dónde.
El simple hecho del asunto es que el significado de this cambia según el lugar donde se encuentre.

Esto es realmente frustrante. Bien, cambia, es Foo , es un enlace estático, etc. etc. etc. ¿Cuál es la firma de tipo? ¿Qué miembros tiene this ? Estás hablando de todo bajo el sol, cuando todo lo que necesito de ti es una firma tipográfica simple para lo que this hay dentro hello .

new LoggableFoo().hello() llama a un método void que llama a un método que de otro modo sería inalcanzable, escrito a través de Greeter.

Esto no es lo mismo que inalcanzable. Cada método accesible es "de otro modo inalcanzable" cuando se descartan las rutas a través de las cuales es accesible.

Si hubieras hecho:

Pero esto es literalmente lo que he hecho. Ese es exactamente mi fragmento de código, pegado nuevamente, precedido por una explicación del ejemplo que construí para usted. Incluso lo pasé por un verificador de diferencias para asegurarme de que no estaba tomando píldoras locas, y la única diferencia es el comentario que eliminaste.

Y, nuevamente, hello() { return this.foo(); } no es un TypeError, ¿por qué sería?

Porque usted (y otros aquí) quieren que el tipo de this sea ​​el tipo de retorno instanciado del decorador, que es { hello(): void } (observe la ausencia de un miembro de foo ). Si desea que los miembros de Foo puedan ver this como el tipo de devolución del decorador, el tipo de this dentro hello será { hello(): void } . Si es { hello(): void } , obtengo un tipo de error. Si recibo un error de tipo, me entristece, porque mi código funciona bien.

Si dice que no es un error de tipo, está abandonando su propio esquema para suministrar el tipo de this a través del tipo de retorno del decorador. El tipo de this es entonces { hello(): string; bar(): string } , independientemente de lo que devuelva el decorador. Es posible que tenga algún esquema alternativo para producir el tipo de this que evite este problema, pero debe especificar cuál es.

Parece que no entiende que, después de que se ejecutan los decoradores, Foo puede hacer referencia a algo totalmente separado de lo que se definió originalmente.

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

Supongo que encuentra algo extraño en que this sea ​​algo diferente dentro de Foo arriba, de lo que es en instancias que se crean posteriormente a partir de lo que finalmente es Foo (después de que se ejecutan los decoradores).

No estoy muy seguro de qué decirte; así es como trabajan los decoradores. Mi único argumento es que TypeScript debería coincidir más con lo que está sucediendo.

Para decirlo de otra manera, las firmas tipográficas dentro del Foo (original) serán diferentes de lo que Foo es/produce una vez que los decoradores han corrido.

Para tomar prestada una analogía de otro lenguaje, dentro del Foo decorado esto estaría haciendo referencia al equivalente de un tipo anónimo de C#: un tipo totalmente real, que por lo demás es válido, solo que no se puede hacer referencia a él directamente. Parecería extraño obtener los errores y no errores descritos anteriormente, pero nuevamente, así es como funciona. Los decoradores nos dan un tremendo poder para hacer cosas extrañas como esta.

Supongo que encuentra algo extraño en que esto sea algo diferente dentro de Foo arriba, de lo que es en instancias que se crean posteriormente a partir de lo que finalmente es Foo (después de que los decoradores se ejecutan).

No. No encuentro ninguna extrañeza en eso, porque es exactamente lo que estaba proponiendo hace 200 comentarios. ¿Has leído algo de la discusión anterior?

El fragmento que ha publicado es totalmente incontrovertible. Las personas con las que no estaba de acuerdo, y a cuya ayuda saltaste, también quieren lo siguiente:

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

No estoy de acuerdo con que sea posible hacer esto de manera sólida, y he estado tratando desesperadamente de descubrir cómo funcionaría tal sugerencia. A pesar de mis mejores esfuerzos, no puedo extraer una lista completa de los miembros que se espera que tenga this , o cómo se construiría esta lista según la propuesta.

Para decirlo de otra manera, las firmas tipográficas dentro del Foo (original) serán diferentes de lo que Foo es/produce una vez que los decoradores han corrido.

Entonces, ¿por qué estás discutiendo conmigo? Dije: "Estoy de acuerdo en que el nuevo Foo().foo es un error de tipo y es fácil de solucionar. No estoy de acuerdo en que devuelva this.foo; ser un error de tipo es un error". Análogamente, en su ejemplo, estoy de acuerdo en que new Foo().x() es un error de tipo es un error, pero this.x() no lo es.

¿Ves cómo hay dos comentarios en el fragmento en la parte superior de esta página?

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

^ Ese es el que encuentro problemático. O acepta que el tipo de retorno del decorador no debe presentarse en this , y solo en new Foo() , en cuyo caso ambos estamos discutiendo sobre nada. O no está de acuerdo y también quiere esa función, en cuyo caso el fragmento de su comentario anterior es irrelevante.

Finalmente entiendo tu punto. Fue extremadamente difícil para mí obtener esto de su código de Greeter, pero ahora estoy rastreando; Te agradezco por ser tan paciente.

Diría que la única solución sensata sería que Foo ( dentro de Foo) admitiera la unión de tipos (derp, quise decir intersección) del Foo original, y lo que sea que devuelva el último decorador. Tienes que apoyar el Foo original para ejemplos locos como tu Greeter, y definitivamente necesitas apoyar lo que sea que regrese el último decorador, ya que ese es el objetivo de usar decoradores (según los muchos comentarios anteriores).

Así que sí, desde mi ejemplo más reciente, dentro de Foo x, y, z, a, b, c todo funcionaría. Si hubiera dos versiones de a , admita ambas.

@arackaf np, gracias también por su paciencia. Mis ejemplos no han sido los más claros porque solo estoy publicando todo lo que puedo cocinar en el patio de recreo que demuestra cómo se rompe. Es difícil para mí pensar en ello sistemáticamente.

Diría que la única solución sensata sería que Foo (dentro de Foo) admitiera la unión de tipos del Foo original, y lo que sea que devuelva el último decorador.

Ok increíble, así que estamos entrando en los detalles ahora. Corrígeme si me equivoco, pero cuando dices "unión", quieres decir que debería tener el tipo de miembros de ambos, es decir, debería ser A & B . Así que queremos que this sea typeof(new OriginalClass()) & typeof(new (decorators(OriginalClass))) , donde decorators es la firma tipográfica compuesta de todos los decoradores . En lenguaje sencillo, queremos que this sea ​​la intersección del tipo instanciado de la "clase original" y el tipo instanciado de la "clase original" pasado por todos los decoradores.

Hay dos problemas con esto. Una es que, en casos como mi ejemplo, esto solo le permite acceder a miembros inexistentes. Podría agregar un montón de miembros en el decorador, pero si intentara acceder a ellos en mi clase usando this.newMethod() , simplemente vomitaría en tiempo de ejecución. newMethod solo se agrega a la clase devuelta por la función de decorador, los miembros de la clase original no tienen acceso a ella (a menos que use específicamente el patrón return class extends OriginalClass { newMethod() { } } ).

El otro problema es que la "clase original" no es un concepto bien definido. Si puedo acceder a los miembros decorados desde this , también puedo usarlos como parte de las declaraciones de devolución y, por lo tanto, pueden ser parte de la API pública de la "clase original". Estoy un poco agitando las manos aquí, y estoy demasiado agotado para pensar en ejemplos concretos, pero creo que si lo pensamos bien, podríamos encontrar ejemplos sin sentido. Tal vez podría solucionar esto encontrando una forma de segregar a los miembros que no devuelven cosas a las que accedieron desde this o al menos para los que el tipo de devolución no se infiere como resultado de devolver this.something() .

@masaeedu sí, me corregí en lo de la unión/intersección antes de tu respuesta. Es contrario a la intuición para alguien nuevo en TS.

Entendido en el resto. Honestamente, los decoradores generalmente no devolverán un tipo completamente diferente, generalmente solo aumentarán el tipo que se pasó, por lo que la intersección "simplemente funcionará" de manera segura la mayor parte del tiempo.

Diría que los errores de tiempo de ejecución de los que habla serían raros y el resultado de algunas malas decisiones de desarrollo a propósito. No estoy seguro de que realmente deba preocuparse por esto, pero, si realmente es un problema, diría que simplemente usar lo que devolvió el último decorador sería un segundo lugar decente (así que sí, una clase podría ver un TypeError por tratando de usar un método que se defina a sí mismo: no es ideal, pero sigue siendo un precio que vale la pena pagar para que los decoradores trabajen).

Pero realmente, creo que no vale la pena prevenir los errores de tiempo de ejecución en los que está pensando, ciertamente a expensas de que los decoradores funcionen correctamente. Además, es bastante fácil producir errores de tiempo de ejecución en TS si es descuidado o tonto.

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

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

En cuanto a su segunda objeción

También puedo usarlos como parte de las declaraciones de devolución y, por lo tanto, pueden ser parte de la API pública de la "clase original".

Me temo que no veo ningún problema con eso. Quiero que cualquier cosa que se agregue a la clase a través de un decorador sea absolutamente un ciudadano de primera clase. Tengo curiosidad por saber cuáles serían los posibles problemas.

Creo que otra buena opción sería implementar esto solo parcialmente.

Actualmente, las clases siempre se escriben tal como están definidas. Entonces, dentro de Foo, esto se basa en cómo se define foo, independientemente de los decoradores. Sería una gran, gran mejora "simplemente" extender eso para algún subconjunto útil de casos de uso de decoradores (es decir, los más comunes)

¿Qué sucede si permite que la clase se extienda (desde la perspectiva de this dentro de la clase) si y solo si el decorador devuelve algo que extiende el original, es decir, para

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

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

Haga que funcione y brinde soporte de primera clase para blah y cualquier otra cosa agregada por el decorador. Y para los casos de uso que hacen cosas locas como devolver una clase completamente nueva (como su Greeter), simplemente continúe con el comportamiento actual e ignore lo que está haciendo el decorador.


Por cierto, independientemente de lo que elijas, ¿cómo anotaría eso? ¿Se puede anotar esto actualmente? Lo intenté

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

así como muchas variaciones sobre ese tema, pero TypeScript no tenía nada de eso :)

@arackaf Un decorador es solo una función. En el caso general, lo obtendrá de un archivo .d.ts en alguna parte, y no tendrá idea de cómo se implementa. No sabe si la implementación está devolviendo una clase completamente nueva, agregando/quitando/reemplazando miembros en el prototipo de la clase original y devolviéndolo, o extendiendo la clase original. Todo lo que tiene disponible es el tipo de retorno estructural de la función.

Si desea vincular de alguna manera a los decoradores con la herencia de clases, primero debe proponer un concepto de lenguaje separado para JS. La forma en que trabajan los decoradores hoy en día no justifica la mutación this en el caso general. Como ejemplo, personalmente siempre prefiero la composición a la herencia, y siempre haría:

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

Este no es un experimento académico loco, es un enfoque estándar para hacer mixins, y rompe con el ejemplo que te di. De hecho, casi cualquier cosa, excepto return class extends Class se rompe con el ejemplo que le di, y en muchos casos las cosas se equilibrarán con return class extends Class .

Vas a tener que saltar a través de todo tipo de aros y contorsiones para hacer que esto funcione, y el sistema de tipos luchará contigo en cada paso del camino, porque lo que estás haciendo no tiene sentido en el caso general. Más importante aún, una vez que lo implemente, todos tendrán que maniobrar acrobáticamente alrededor de este rincón sin sentido del sistema de tipos cada vez que intenten implementar algún otro concepto complejo (pero sólido).

El hecho de que tenga este caso de uso que considere importante (y lo he intentado varias veces en este hilo para demostrar formas alternativas de expresar lo que desea con claridad en el sistema de tipo existente), no significa que lo único correcto hacer es seguir adelante con el enfoque sugerido a toda costa. Puede fusionar interfaces arbitrarias con su clase, incluidos los tipos de retorno de las funciones de decorador, por lo que no es imposible llegar a ninguna parte si insiste en usar los decoradores de la forma en que los está usando.

@arackaf esto:

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

Debería funcionar bien en la cláusula extends:

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

Con una semántica mucho más sencilla y ya definida en el sistema de tipos. Esto ya funciona. ¿Qué problema está tratando de resolver mediante subclases de la clase declarada, en lugar de crear una superclase para ella?

@masaeedu

La forma de trabajar de los decoradores hoy en día no justifica mutar esto en el caso general.

El uso principal de los decoradores (de clase) es absoluta y positivamente mutar el valor de this dentro de la clase, de una forma u otra. El @connect @observer MobX toman una clase y escupen una NUEVA versión de la clase original, con características adicionales. En esos casos particulares, no creo que la estructura real de this cambie (solo la estructura de this.props ), pero eso es irrelevante.

Es un caso de uso bastante común (como puede ver en los comentarios anteriores) usar un decorador para mutar una clase de alguna manera). Para bien o para mal, a la gente simplemente le desagradan las alternativas sintácticas.

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

Opuesto a

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

Ahora, si un decorador hace

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

o

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

no debería importar De una forma u otra, el caso de uso que muchos realmente claman es poder decirle a TypeScript, mediante las anotaciones necesarias, sin importar cuán inconveniente sea para nosotros, que function c toma una clase C in y devuelve una clase que tiene la estructura C & {blah(): void} .

Eso es para lo que muchos de nosotros estamos usando decoradores hoy en día. Realmente queremos integrar un subconjunto útil de este comportamiento en el sistema de tipos.

Se demuestra fácilmente que los decoradores pueden hacer cosas extrañas que serían imposibles de rastrear para el sistema de tipos. ¡Multa! Pero debe haber alguna forma de anotar que

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

es válido. No sé si requerirá una nueva sintaxis de anotación de tipo para c , o si la sintaxis de anotación de tipo existente podría ponerse en servicio en c para lograr esto, pero solo corrigiendo el uso general case (y dejar los bordes como están, sin ningún efecto en la clase original) sería una gran ayuda para los usuarios de TS.

@justinfagnani para tu información

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

produce

El tipo 'typeof (clase anónima)' no se puede asignar al tipo 'T & (nuevo () => { blah(): void; })'.
El tipo 'typeof (clase anónima)' no se puede asignar al tipo 'T'.

en el patio de juegos de TS. No estoy seguro si tengo algunas opciones mal configuradas.

¿Qué problema está tratando de resolver mediante subclases de la clase declarada, en lugar de crear una superclase para ella?

Simplemente estoy tratando de usar decoradores. Felizmente los he estado usando en JS durante más de un año; estoy ansioso por que TS se ponga al día. Y ciertamente no estoy casado con la subclasificación. A veces, el decorador simplemente muta el prototipo de la clase original para agregar propiedades o métodos. De cualquier manera, el objetivo es decirle a TypeScript que

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

dará como resultado una clase con nuevos miembros, creada por c .

@arackaf Nos estamos desincronizando de nuevo. El debate no es sobre esto:

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

contra esto:

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

Espero que pueda usar el primero y aún tenga la tipificación adecuada de new Foo() . El debate es sobre esto:

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

contra esto:

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

Este último funcionará sin romper el sistema de tipos. Lo primero es problemático en varias formas que ya he ilustrado.

@masaeedu , ¿realmente no hay casos de uso restringido en los que se pueda hacer que el primero funcione?

Para ser precisos: ¿no hay anotaciones que TypeScript pueda decirnos que agreguemos a a , b y c de su ejemplo anterior para que el sistema de tipos trate el primero? indistinguible de este último?

Eso sería un gran avance para TypeScript.

@arackaf lo siento, debe hacer que el parámetro de tipo se refiera al tipo de constructor, no al tipo de instancia:

Esto funciona:

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

Simplemente estoy tratando de usar decoradores.

Ese no es realmente un problema que estés tratando de resolver, y es una tautología. Es como Q: "¿qué estás tratando de hacer con ese martillo?" R: "Solo usa el martillo".

¿Hay algún caso real que tenga en mente en el que generar una nueva superclase con la extensión prevista no funcione? Porque TypeScript admite eso ahora, y como dije, es mucho más fácil razonar sobre el tipo de clase desde dentro de la clase en ese caso.

Miré la anotación @observer de Mobx y parece que no cambia la forma de la clase (y podría ser fácilmente un decorador de métodos en lugar de un decorador de clases, ya que solo envuelve render() ).

@connect es en realidad un gran ejemplo de las complejidades de escribir decoradores correctamente, porque parece que @connect ni siquiera devuelve una subclase de la clase decorada, sino una clase completamente nueva que envuelve la clase decorada, pero las estáticas se copian, por lo que el resultado descarta la interfaz del lado de la instancia y conserva el lado estático que TS ni siquiera verifica en la mayoría de los casos. Parece que @connect realmente no debería ser un decorador en absoluto y connect solo debería usarse como un HOC, porque como decorador, es terriblemente confuso.

Felizmente los he estado usando en JS durante más de un año.

Bueno... en realidad no lo has hecho. JS no tiene decoradores. Es probable que haya estado usando una propuesta anterior desaparecida en particular, que se implementa de manera ligeramente diferente en Babel y TypeScript. En otros lugares, estoy abogando por que los decoradores continúen con el proceso de estandarización, pero el progreso se ha ralentizado y no creo que sean una certeza todavía. Muchos argumentan que no deberían agregarse, o que solo deben ser anotaciones, por lo que la semántica aún puede estar en el aire.

Una de las quejas contra los decoradores es exactamente que pueden cambiar la forma de la clase, lo que dificulta el razonamiento estático, y se ha hablado de algunas propuestas al menos para limitar la capacidad de los decoradores para hacer eso, aunque creo que eso fue antes de la la propuesta más reciente tiene lenguaje de especificaciones. Personalmente, sostengo que los decoradores que se comportan bien _no deberían_ cambiar la forma de la clase, de modo que a efectos del análisis estático puedan ignorarse. Recuerde que JS estándar no tiene un sistema de tipos en el que apoyarse para decirle al análisis qué está haciendo la implementación de una función.

  • Solo estoy ansioso por que TS se ponga al día.

No veo cómo TypeScript está atrasado de ninguna manera aquí. Al menos, ¿detrás de qué? Los decoradores funcionan. ¿Hay otro JS escrito donde las clases se comporten de la manera que desea?

Los decoradores ahora están en la etapa 2 y se usan en casi todos los marcos JS.

En cuanto a @connect react-redux tiene alrededor de 2,5 millones de descargas por mes; es increíblemente arrogante escuchar que el diseño es de alguna manera "incorrecto" porque lo habrías implementado de manera diferente.

Los intentos actuales de usar métodos introducidos por un decorador dan como resultado errores de tipo en TypeScript, lo que requiere soluciones como la fusión de la interfaz, agregar manualmente la anotación para los métodos agregados o simplemente no usar decoradores para mutar clases (como si alguien necesitara que le dijeran que podíamos simplemente no usar decoradores).

¿Realmente no hay forma de informar manualmente y minuciosamente al compilador de TS que un decorador está cambiando la forma de una clase? Tal vez esto sea una locura, pero TS no podría simplemente enviar un nuevo decorador.tipo que podríamos usar

sea ​​c: decorador

Y SI (y solo si) se aplican decoradores de ese tipo a una clase, TS considerará que la clase es del tipo Input & { blah() } ?

Literalmente, todos saben que simplemente no podemos usar decoradores. La mayoría también sabe que a un grupo vocal no le gustan los decoradores. Esta publicación trata sobre cómo TS podría, de alguna manera, hacer que su sistema de tipos entienda que un decorador está mutando una definición de clase. Eso sería una gran ayuda para mucha gente.

Para ser precisos: ¿no hay anotaciones que TypeScript pueda decirnos que agreguemos a a, b y c de su ejemplo anterior para que el sistema de tipos trate el primero de manera indistinguible del segundo?

Sí, es muy fácil declarar cualquier forma para Foo que desee, solo necesita usar la combinación de interfaz. Solo necesitas hacer:

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

En este momento, probablemente deba esperar que cualquier biblioteca de decoradores que esté utilizando exporte tipos parametrizados correspondientes a lo que están devolviendo, o deberá declarar los miembros que está agregando usted mismo. Si obtenemos #6606, puedes hacer interface Foo extends typeof(a(b(c(Foo)))) .

Los intentos actuales de usar métodos introducidos por un decorador dan como resultado errores de tipo en TypeScript, lo que requiere soluciones como la fusión de la interfaz, agregar manualmente la anotación para los métodos agregados o simplemente no usar decoradores para mutar clases (como si alguien necesitara que le dijeran que podíamos simplemente no usar decoradores).

No mencionó la opción de decorar la clase que está escribiendo cuando es independiente de los miembros presentados por el decorador, y hacer que extienda una clase decorada cuando no lo es. Así es como usaré decoradores al menos.

¿Podría darme un fragmento de código de una biblioteca que esté usando en este momento donde este es un punto débil?

Lo siento, no seguí tu último comentario, pero tu comentario anterior, con la fusión de la interfaz, es exactamente como lo estoy solucionando.

Actualmente, mi decorador también necesita exportar un Tipo, y uso la combinación de interfaz exactamente como usted mostró, para indicarle a TS que el decorador está agregando nuevos miembros. Es la capacidad de deshacerse de este repetitivo agregado que muchos están ansiosos por hacer.

@arackaf Quizás lo que realmente desea es una forma de fusión de declaraciones para interactuar con la tipificación contextual de los argumentos de función.

Seguro. Quiero la capacidad de usar un decorador en una clase y, conceptualmente, en función de cómo he definido el decorador, que se produzca una fusión de interfaz, lo que da como resultado que se agreguen miembros adicionales a mi clase decorada.

No tengo idea de cómo haría para anotar a mi decorador para que eso suceda, pero no soy muy exigente: cualquier sintaxis que una futura versión de TS me diera para afectar eso me haría increíblemente feliz :)

Los decoradores ahora están en la etapa 2 y se usan en casi todos los marcos JS.

Y solo están en la etapa 2 y se han estado moviendo extremadamente lentamente. Tengo muchas ganas de que sucedan los decoradores, y estoy tratando de descubrir cómo puedo ayudarlos a avanzar, pero todavía no son una certeza. Todavía sé de personas que podrían tener influencia en el proceso que se oponen a ellos, o quieren que se limiten a las anotaciones, aunque no creo que interfieran. Mi punto sobre las implementaciones actuales del decorador del compilador que no son JS se mantiene.

En cuanto a @connect , react-redux tiene alrededor de 2,5 millones de descargas por mes; es increíblemente arrogante escuchar que el diseño es de alguna manera "incorrecto" porque lo habrías implementado de manera diferente.

Por favor, abstente de ataques personales como llamarme arrogante.

  1. No te ataqué personalmente, así que no me ataques personalmente.
  2. No ayuda a su argumento, en absoluto.
  3. La popularidad de un proyecto no debería excluirlo de la crítica técnica.
  4. ¿Estamos hablando sobre la misma cosa? redux-connect-decorator tiene 4 descargas al día. La función connect() de react-redux parece un HOC, como sugerí.
  5. Creo que mi opinión de que los decoradores que se portan bien no deberían cambiar la forma pública de una clase, y ciertamente deberían reemplazarla con una clase no relacionada, es bastante razonable y encontraría suficiente acuerdo en la comunidad JS. Está bastante lejos de ser arrogante, incluso si no estás de acuerdo.

Los intentos actuales de usar métodos introducidos por un decorador dan como resultado errores de tipo en TypeScript, lo que requiere soluciones como la fusión de la interfaz, agregar manualmente la anotación para los métodos agregados o simplemente no usar decoradores para mutar clases (como si alguien necesitara que le dijeran que podíamos simplemente no usar decoradores).

No es que solo estemos sugiriendo no usar decoradores en absoluto, es que creo que @masaeedu está diciendo que, con el fin de modificar un prototipo de forma segura, tenemos mecanismos existentes: herencia y aplicación mixta. Estaba tratando de preguntar por qué su ejemplo d no era adecuado para ser usado como una mezcla, y no dio una respuesta, excepto para decir que solo quería usar decoradores.

Eso está bien, supongo, pero parece limitar la conversación de manera bastante crítica, así que me retiraré nuevamente.

@justinfagnani estaba hablando

import {connect} from 'react-redux'

connect solo hay una función que toma una clase (componente) y escupe una diferente, como dijiste anteriormente.

Actualmente, tanto en Babel como en TypeScript, tengo la opción de usar connect así

class UnConnectedComponent extends Component {
}

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

O así

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

Yo prefiero lo último, pero si tú o alguien más prefiere lo primero, que así sea; muchos caminos conducen a Roma. igualmente prefiero

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

encima

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

o

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

No respondí explícitamente a su pregunta solo porque pensé que la razón por la que estaba usando decoradores estaba clara, pero lo diré explícitamente: prefiero la ergonomía y la sintaxis que brindan los decoradores. Todo este hilo se trata de tratar de obtener alguna capacidad para decirle a TS cómo nuestros decoradores están mutando la clase en cuestión para que no necesitemos una fusión de interfaz repetitiva.

Como dijo OP hace mucho tiempo, solo queremos

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'

solo para trabajar El hecho de que existan otras alternativas a esta sintaxis específica es obvio e irrelevante. El hecho de que a muchos en la comunidad JS/TS no les guste esta sintaxis en particular también es obvio e irrelevante.

Si TS pudiera brindarnos alguna forma, por restringida que sea, de hacer que esta sintaxis funcione, sería un gran beneficio para el lenguaje y la comunidad.

@arackaf Olvidó mencionar cómo usar connect requiere acceder a miembros adicionales en this . AFAICT, se supone que la implementación de su componente es totalmente independiente de lo que hace connect , solo usa su propio props y state . Entonces, en ese sentido, la forma en que connect está diseñado está más de acuerdo con el uso de decoradores de @justinfagnani que con el tuyo.

Realmente no. Considerar

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

Entonces despúes

<Foo />

eso debería ser válido: los accesorios de PropsShape provienen de la tienda Redux, pero TypeScript no lo sabe, ya que se sale de la definición original de Foo, por lo que termino recibiendo errores por falta de accesorios, y necesito convertirlo como any .

Pero para ser claros, el caso de uso preciso proporcionado originalmente por OP, y duplicado en mi segundo comentario anterior aquí, es extremadamente común en nuestra base de código y, por lo que parece, también en muchos otros. Literalmente usamos cosas como

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

y tiene que agregar alguna combinación de interfaz para satisfacer TypeScript. Sería maravilloso tener una forma de hornear esa interfaz que se fusiona con la definición del decorador de forma transparente.

Seguimos dando vueltas con esto. Estamos de acuerdo en que el tipo de Foo debe ser el tipo de devolución de connect . Estamos totalmente de acuerdo en esto.

Donde diferimos es si los miembros dentro Foo necesitan pretender que this es algún tipo de fusión recursiva del tipo de retorno del decorador y el "Foo original", sea lo que sea que eso signifique. No ha demostrado un caso de uso para esto.

Donde diferimos es si los miembros dentro de Foo necesitan fingir que se trata de una especie de fusión recursiva del tipo de retorno del decorador y el "Foo original", sea lo que sea que eso signifique. No ha demostrado un caso de uso para esto.

Yo tengo. Vea mi comentario anterior y, de hecho, el código original de OP. Este es un caso de uso extremadamente común.

Muéstrenme qué agrega connect al prototipo al que espera que acceda dentro del componente. Si la respuesta es "nada", no vuelva a mencionar connect , ya que es totalmente irrelevante.

@masaeedu bastante justo, y estoy de acuerdo. Principalmente respondía a las afirmaciones de @justinfagnani sobre cómo los decoradores no deberían modificar la clase original, connect se diseñó mal, etc., etc.

Simplemente estaba demostrando la sintaxis que yo y muchos otros preferimos, a pesar del hecho de que existen otras opciones.

Para este caso, sí, no conozco bibliotecas npm populares que tomen una clase y escupan una nueva clase con nuevos métodos, que se usarían dentro de la clase decorada. Esto es algo que yo y otros hacemos con frecuencia, en nuestro propio código, para implementar nuestros propios mixins, pero realmente no tengo un ejemplo en npm.

Pero en realidad no se discute que este es un caso de uso común, ¿verdad?

@arackaf No, no lo hace porque no está accediendo a ningún miembro en this presentado por @connect . De hecho, estoy bastante seguro de este fragmento preciso:

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

compilará sin problemas en TypeScript hoy. El fragmento que ha pegado no tiene nada que ver con la función que está solicitando.

@masaeedu , lo siento mucho, me di cuenta de que estaba equivocado y eliminé ese comentario. Sin embargo, no lo suficientemente rápido como para evitar que pierdas el tiempo :(

@arackaf No sé qué tan común es un patrón, y personalmente no lo he usado ni he usado ninguna biblioteca que lo use. Usé Angular 2 por un tiempo, así que no es como si nunca hubiera usado decoradores en mi vida. Es por eso que pedí ejemplos específicos del uso de una biblioteca donde la imposibilidad de acceder a los miembros introducidos por el decorador en el decorado es un punto doloroso.

En todos los casos en los que los miembros de una clase esperan algo de this , debe haber algo en el cuerpo de la clase o en la cláusula extends de la clase que satisfaga el requisito. Así ha funcionado siempre, incluso con los decoradores . Si tiene un decorador que agrega alguna funcionalidad de la que depende una clase, la clase debe extender lo que devuelva el decorador, y no al revés. Es solo sentido común.

No estoy seguro de por qué esto es de sentido común. Este es el código de nuestra base de código actualmente. Tenemos un decorador que agrega un método al prototipo de una clase. Errores de TypeScript. En ese momento acabo de lanzar esta declaración allí. Podría haber usado la fusión de interfaz.

Pero sería bueno no usar nada en absoluto. Sería muy, muy bueno poder decirle a TypeScript que "este decorador aquí agrega este método a cualquier clase a la que se aplique; permita que this dentro de la clase también acceda a él".

Muchos otros en este hilo parecen estar usando decoradores de manera similar, así que espero que consideres que podría no ser de sentido común que esto no debería funcionar.

image

@arackaf Sí, entonces deberías estar haciendo export class SearchVm extends (@mappable({...}) class extends SearchVmBase {}) . Es SearchVm lo que depende de la mapeabilidad, no al revés.

clase de exportación SearchVm extiende (@mappable({...}) clase extiende SearchVmBase {})

El objetivo de este hilo es EVITAR frases repetitivas como esa. Los decoradores proporcionan un DX mucho, mucho mejor que tener que hacer cosas así. Hay una razón por la que elegimos escribir código como lo he hecho yo, en lugar de eso.

Si lee los comentarios de este hilo, espero que se convenza de que muchas otras personas están tratando de usar la sintaxis de decorador más simple y, por lo tanto, reconsiderar lo que "deberíamos" hacer, o lo que es "sentido común", etc.

Lo que quieres no tiene nada que ver específicamente con los decoradores. Su problema es que el mecanismo de TypeScript para decir "oye, TypeScript, le están sucediendo cosas a esta estructura de las que no sabe nada, déjeme contarle todo al respecto", actualmente es demasiado limitado. En este momento solo tenemos la combinación de interfaces, pero desea que las funciones puedan aplicar la combinación de interfaces a sus argumentos, lo que no está relacionado específicamente con los decoradores de ninguna manera.

En este momento solo tenemos la combinación de interfaces, pero desea que las funciones puedan aplicar la combinación de interfaces a sus argumentos, lo que no está relacionado específicamente con los decoradores de ninguna manera.

Está bien. Lo suficientemente justo. ¿Quieres que abra un número separado, o quieres cambiar el nombre de este, etc.?

@arackaf El único "repetitivo" es el hecho de que tenía class { } , que es a) 7 caracteres fijos, sin importar cuántos decoradores use b) como resultado del hecho de que los decoradores no pueden (todavía ) ser aplicado a expresiones de retorno de clase arbitrarias, y c) porque específicamente quería usar decoradores para su beneficio. Esta formulación alternativa:

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

no es más detallado que lo que está haciendo originalmente.

Está bien. Lo suficientemente justo. ¿Quieres que abra un número separado, o quieres cambiar el nombre de este, etc.?

Un tema aparte estaría bien.

no es más detallado que lo que estás haciendo originalmente

No es más detallado, pero realmente espero que considere el hecho de que a muchos simplemente no les gusta esa sintaxis. Para bien o para mal, muchos prefieren la oferta de los decoradores DX.

Un tema aparte estaría bien.

Voy a escribir uno mañana, y enlazar a éste para el contexto.

Gracias por ayudar a resolver esto :)

Si puedo entrar, @masaeedu no estoy de acuerdo con esta declaración:

Lo que quieres no tiene nada que ver específicamente con los decoradores. Su problema es que el mecanismo de TypeScript para decir "oye, TypeScript, le están sucediendo cosas a esta estructura de las que no sabe nada, déjeme contarle todo al respecto", actualmente es demasiado limitado. En este momento solo tenemos la combinación de interfaces, pero desea que las funciones puedan aplicar la combinación de interfaces a sus argumentos, lo que no está relacionado específicamente con los decoradores de ninguna manera.

Seguramente TypeScript podría mirar el tipo de retorno del decorador de clase para determinar qué tipo debería ser la clase mutada. No es necesario "aplicar la fusión de interfaz a los argumentos". Entonces, tal como lo veo, tiene que ver completamente con los decoradores.

Además, el caso de que TypeScript no sepa que un componente conectado ya no requiere accesorios es un aspecto que me desconectó de usar Redux con React y TypeScript.

@codeandcats He estado dando vueltas con esto desde que comenzó el hilo, y realmente ya no puedo hacerlo. Mire cuidadosamente la discusión anterior y trate de entender en qué no estaba de acuerdo con @arackaf .

Acepto que cuando haces <strong i="8">@bar</strong> class Foo { } , el tipo de Foo debe ser el tipo de retorno de bar . No estoy de acuerdo con que this dentro Foo deba verse afectado de ninguna manera. Usar connect es totalmente irrelevante para el punto de discusión aquí, porque connect no espera que el componente decorado conozca y use los miembros que está agregando al prototipo.

Puedo entender que estés frustrado @masaeedu , pero no asumas que no he estado expresando activamente mi opinión en este ir y venir que has tenido durante las últimas 48 horas que no he estado siguiendo la discusión, así que por favor. Ahórrame la condescendencia compañero.

Según tengo entendido, piensas que dentro de una clase decorada no debería saber acerca de ninguna mutación hecha en sí misma por los decoradores, pero fuera debería. No estoy en desacuerdo con eso. El hecho de que @arackaf piense que dentro de una clase debería ver la versión mutada no tiene nada que ver con mi comentario.

De cualquier manera, TypeScript podría usar el tipo devuelto por una función de decorador como fuente de verdad para una clase. Y, por lo tanto, es un problema de Decorator, no una nueva funcionalidad de mutación de argumentos extraños como sugieres, que honestamente solo suena como un intento de hombre de paja.

Sin embargo, @Zalastax que está usando la función de decorador como función, no como decorador. Si tiene varios decoradores que necesita aplicar, debe anidarlos, lo que técnicamente ya no es detallado pero no es tan sintácticamente agradable, ¿sabe a qué me refiero?

condescendencia etc

Ruido.

El hecho de que @arackaf piense que dentro de una clase debería ver la versión mutada no tiene nada que ver con mi comentario.

Es el punto central del párrafo al que está respondiendo, por lo que si está hablando de otra cosa, es irrelevante. Lo que @arackaf quiere es, más o menos, la fusión de la interfaz en los argumentos de la función. Esto se aborda mejor mediante una función independiente de los decoradores. Lo que quiere (que aparentemente es idéntico a lo que yo quiero) es corregir el error en TypeScript donde <strong i="12">@foo</strong> class Foo { } no da como resultado la misma firma de tipo para Foo como const Foo = foo(class { }) . Sólo este último está específicamente relacionado con los decoradores.

Según tengo entendido, piensas que dentro de una clase decorada no debería saber acerca de ninguna mutación hecha en sí misma por los decoradores, pero fuera debería. no estoy en desacuerdo con eso

Si ha decidido hablar sobre algo en lo que estamos de acuerdo, pero antecede con "No estoy de acuerdo con esta declaración", entonces está perdiendo el tiempo.

¿Puedo pedir papas fritas con esta sal?

Oh, ya veo, cuando patrocinas a tus compañeros está bien, pero si la gente te molesta, eso es ruido.

No veo por qué se necesita su concepto de "mutación de argumento" para implementar lo que propuso @arackaf .

Independientemente de si estamos de acuerdo con @arackaf o no, en cualquier caso, TypeScript podría simplemente inferir el tipo real del resultado del decorador, en mi humilde opinión.

Disculpas si te sentiste condescendiente; la intención era llevarte a
en realidad, un hilo divagante gigante que cualquiera podría ser perdonado
rozando Mantengo esto, ya que todavía está bastante claro que no eres consciente
de los detalles y están "en desacuerdo" conmigo debido a un malentendido de
Mi posición.

Por ejemplo, su pregunta sobre por qué la interfaz se fusiona en función
argumentos resuelve el problema de Adam se responde mejor observando los
discusión con Adam, donde dice que ya está usando la combinación de interfaces,
pero le resulta molesto tener que hacer esto en cada sitio de declaración.

Creo que todos los aspectos técnicos de esto han sido golpeados hasta la muerte. I
no quiero que el hilo esté dominado por disputas interpersonales, así que esto
va a ser mi ultimo post.

Es cierto, hay dos errores con los decoradores: no se respeta el tipo de devolución y, dentro de la clase, no se respetan los miembros agregados ( this dentro de la clase). Y sí, estoy más preocupado por lo segundo, ya que lo primero es más fácil de solucionar.

Abrí un caso separado aquí: https://github.com/Microsoft/TypeScript/issues/16599

Hola a todos, me perdí esta conversación durante el fin de semana, y tal vez no haya muchas razones para volver a mencionarla ahora; pero quiero recordarles a todos que en el fragor de este tipo de discusiones, todos los que intervienen suelen tener buenas intenciones. Mantener un tono respetuoso puede ayudar a aclarar y orientar la conversación, y puede fomentar nuevos colaboradores. Hacer esto no siempre es fácil (especialmente cuando Internet deja la tonalidad en manos del lector), pero creo que es importante para escenarios futuros. 😃

¿Cuál es el estado de esto?

@alex94puchades todavía es una propuesta de etapa 2 , por lo que probablemente aún nos quede un tiempo. TC39 parece tener algo de movimiento , al menos.

Según este comentario , parece que podría proponerse para la etapa 3 a partir de noviembre.

una forma alternativa de cambiar la firma de una función por decorador

agregar una función wapper vacía

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

añadir definición

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

resultado

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

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

/silbido

Si alguien está buscando una solución a esto, a continuación se me ocurre una solución alternativa.

No es la mejor solución porque requiere un objeto anidado, pero para mí funciona bien porque en realidad quería que las propiedades del decorador estuvieran en un objeto y no solo planas en la instancia de la clase.

Esto es algo que estoy usando para mis modales angulares 5.

Imagina que tengo un decorador @ModalParams(...) que puedo usar en ConfirmModalComponent personalizado. Para que las propiedades del decorador @ModalParams(...) aparezcan dentro de mi componente personalizado, necesito extender una clase base que tenga una propiedad a la que el decorador asignará sus valores.

Por ejemplo:

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

Nuevamente, no es muy bonito, pero funciona muy bien para mi caso de uso, así que pensé que alguien más podría encontrarlo útil.

@lansana pero no entiendes los tipos, ¿verdad?

@confraria Desafortunadamente no, pero puede haber una manera de lograrlo si implementa una clase genérica Modal que extiende. Por ejemplo, algo como esto podría funcionar (no probado):

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

:/ sí, pero luego el tipo está desacoplado del decorador y ciertamente no puede usar dos de ellos .. :( además, obtendría errores si la clase no implementara los métodos .. :( no creo que haya es una forma de obtener los tipos correctos con este patrón en este momento

Sí, sería genial si esto fuera posible, hace que TypeScript sea mucho mejor y expresivo. Esperemos que algo salga de esto pronto.

@lansana Sí, el punto es que sería bueno que los decoradores de clase pudieran cambiar la firma de la clase por sí mismos, sin requerir que la clase amplíe o implemente nada más (ya que eso es una duplicación de esfuerzo y tipo de información) .

Nota al margen: en su ejemplo allí, tenga en cuenta que params sería estático en todas las instancias de clases de componentes modales decoradas, ya que es una referencia de objeto. Aunque tal vez eso es por diseño. : - ) Pero yo divago...

Editar: mientras pienso en esto, puedo ver las desventajas de permitir que los decoradores modifiquen las firmas de clase. Si la implementación de la clase presenta claramente ciertas anotaciones de tipo, pero un decorador es capaz de intervenir y cambiar todo eso, eso sería un truco un poco malo para los desarrolladores. Evidentemente, muchos de los casos de uso se refieren a la incorporación de nueva lógica de clase, por lo que, lamentablemente, muchos de ellos se facilitarían mejor a través de la implementación de la extensión o la interfaz, que también se coordina con la firma de clase existente y genera los errores apropiados donde ocurren las colisiones. Un marco como Angular, por supuesto, hace un uso abundante de decoradores para aumentar las clases, pero el diseño no es para permitir que la clase "gane" o mezcle nueva lógica del decorador que luego puede usar en su propia implementación: es para aislar lógica de clase de la lógica de coordinación del marco. Esa es mi _humilde_ opinión, de todos modos. :-)

Parece mejor usar clases de orden superior en lugar de decoradores + trucos. Sé que la gente quiere usar decoradores para estas cosas, pero usar compose + HOC es el camino a seguir y probablemente seguirá siendo así... para siempre;) Según MS, etc., los decoradores son solo para adjuntar metadatos a las clases y cuando verifique a los grandes usuarios de decoradores como Angular, verá que solo se usan en esta capacidad. Dudo que pueda convencer a los mantenedores de TypeScript de lo contrario.

Es triste y un poco extraño que el equipo de TS haya ignorado durante tanto tiempo una característica tan poderosa, que permitiría una verdadera composición de características y que genera tal compromiso.

Esta es realmente una característica que podría revolucionar la forma en que escribimos código; permitiendo a todos lanzar pequeños mixins en cosas como Bitly o NPM y tener una reutilización de código realmente increíble en Typescript. En mis propios proyectos, haría instantáneamente mi @Poolable @Initable @Translating y probablemente un montón más.

Por favor, todo el poderoso equipo central de TS. "Todo lo que necesita" para implementar es que se respeten las interfaces devueltas.

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

lo que permitiría que este código se ejecute sin quejas:

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

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

@AllNamesRTaken

Aunque estoy de acuerdo con tus puntos, puedes lograr lo mismo así:

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

Y entonces ni siquiera necesitarías una función de inicio, simplemente podrías hacer esto:

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

@lansana
Sí, pero eso contaminaría a mi constructor, que no siempre es muy tonto para lo que quiero.

También tengo una mezcla similar para hacer que cualquier objeto se pueda agrupar y otras cosas. Solo la posibilidad de agregar funcionalidad a través de la composición es lo que se necesita. El ejemplo de inicio es solo una forma necesaria de hacer lo que hizo sin contaminar el constructor, haciéndolo útil con otros marcos.

@AllNamesRTaken No tiene sentido tocar a los decoradores en este momento, ya que ha estado en este estado durante tanto tiempo y la propuesta ya se encuentra en la etapa 2. Espere a que finalice https://github.com/tc39/proposal-decorators y luego lo más probable es que los obtengamos en cualquier forma en la que todos estén de acuerdo.

@Kukkimonsuta
No estoy de acuerdo con usted ya que lo que solicito es puramente sobre el tipo y no sobre la funcionalidad. Por lo tanto, no tiene mucho que ver con las cosas de ES. Mi solución anterior ya funciona, simplemente no quiero tener que enviar a.

@AllNamesRTaken puedes hacer esto _hoy_ con mixins sin ninguna advertencia:

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

En el futuro, si llevamos la propuesta de mixins a JS, podemos hacer esto:

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 fue cómo comencé con estas características y mi implementación en realidad también funciona como usted describe:

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

lo cual es agradable, como una solución alternativa, pero en realidad no elimina los méritos de permitir que los decoradores cambien el tipo en mi opinión.

EDITAR: también pierde el tipeo en el parcial para init.

Con esta característica podrás escribir HoC más fácilmente
Espero que esta función se agregue

@kgtkr esa es la razón principal por la que quiero esto tan desesperadamente...

También hay una pequeña emergencia en las definiciones de react-router porque algunas personas decidieron que la seguridad de tipo es más importante que tener una interfaz de decoración de clase. La razón principal es que withRouter hace que algunos de los accesorios sean opcionales.

Ahora parece haber una pelea, donde las personas se ven obligadas a usar la interfaz de funciones de withRouter en lugar de la decoración .

Comenzar a resolver esta característica haría que el mundo fuera un lugar más feliz.

La misma enemistad* ocurrió hace un tiempo con los tipos de material-ui y withStyle , que fue diseñado para usarse como decorador, pero que, para usar de una manera segura, los usuarios de TypeScript deben usar como una función regular. A pesar de que la mayor parte del tiempo se ha asentado en eso, ¡es una fuente de confusión continua para los recién llegados al proyecto!

* Bueno, "pelea" puede ser una palabra fuerte

He estado viendo esto durante mucho tiempo y, hasta que llegue, es de esperar que otros puedan beneficiarse de mi pequeño truco para lograr este comportamiento de una manera compatible con versiones posteriores. Así que aquí está, implementando un método súper básico de clase de caso de estilo Scala copy ...

Tengo el decorador implementado así:

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

Este código crea una clase anónima con el método copy adjunto. Funciona exactamente como se esperaba en JavaScript cuando se encuentra en un entorno compatible con decoradores.

Para usar esto en TypeScript, y hacer que el sistema de tipos refleje el nuevo método en la clase de destino, se puede usar el siguiente truco:

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

Todas las instancias de MyCaseClass expondrán los tipos del método copy heredado de la clase anónima dentro del decorador CaseClass . Y, cuando TypeScript admite la mutación de tipos declarados, este código se puede modificar fácilmente a la sintaxis de decorador habitual <strong i="18">@CaseClass</strong> etc sin interrupciones inesperadas.

Sería genial ver esto en la próxima versión importante de TypeScript; creo que ayudará a un código más limpio y conciso en lugar de exportar un desorden extraño de clases de proxy.

¿Cuándo estará disponible esta función?

Quería usar un decorador de clase para encargarse de la tarea repetida.
Pero ahora, al inicializar la clase, aparece el siguiente error: 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.

¡Espero que esta función esté disponible pronto! :)

@ iainreid820 , ¿experimentó algún error extraño al usar este enfoque?

¡La larga espera! Mientras tanto, ¿es esto resuelto por algo en la hoja de ruta actual?
¿Como el número 5453 ?

Estoy usando Material UI y tuve que darme cuenta de que TypeScript no es compatible con la sintaxis del decorador para withStyles : https://material-ui.com/guides/typescript/#decorating -components

Corrija esa limitación en la próxima versión de TypeScript. Los decoradores de clase me parecen bastante inútiles en este momento.

Como mantenedor de Morphism Js , esta es una gran limitación para este tipo de biblioteca. Me encantaría evitar que el consumidor de un decorador de funciones tenga que especificar el tipo de destino de la función https://github.com/nobrainr/morphism# --toclassobject-decorator, de lo contrario, usar decoradores en lugar de HOF suena un poco inútil 😕
¿Hay algún plan para abordar esto? ¿Hay alguna manera de ayudar a que esta función suceda? ¡Gracias de antemano!

@bikeshedder ese ejemplo de Material UI es un caso de uso de mezcla de clase, y puede obtener el tipo correcto de mixins. En vez de:

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

escribir:

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

@justinfagnani Eso no funciona para mí:

ss 2018-10-16 at 10 00 47

Aquí está mi código: https://gist.github.com/G-Rath/654dff328dbc3ae90d16caa27a4d7262

@G-Rath Creo que new () => React.Component<Props, State> debería funcionar.

@emyann Sin dados. Actualicé la esencia con el nuevo código, pero ¿es esto lo que quisiste decir?

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

No importa cómo lo formatee, extends withStyles(styles)(...) no suena como una sugerencia adecuada, fundamentalmente. withStyles NO es una combinación de clases.

withStyles toma la clase de componente A y crea la clase B que, cuando se procesa, representa la clase A y le pasa accesorios + el accesorio classes .

Si extiende el valor de retorno de withStyles, entonces está extendiendo el contenedor B , en lugar de implementar la clase A que en realidad recibe la propiedad classes .

No importa cómo lo formatee, extender con Estilos (estilos) (...) no suena como una sugerencia adecuada, fundamentalmente. withStyles NO es una combinación de clases.

Por supuesto. No estoy seguro de por qué hay tanto rechazo aquí.

Dicho esto, los decoradores están muy cerca de la etapa 3, por lo que escuché, así que imagino que TS pronto recibirá el apoyo adecuado aquí.

Sin embargo, escucho a las personas que quieren una sintaxis más limpia, especialmente. al usar la aplicación de múltiples HoC como decoradores. La solución temporal que usamos es envolver varios decoradores dentro de una función de tubería y luego usar esa función pura para decorar el componente. p.ej

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

donde flow es lodash.flow . Muchas bibliotecas de utilidades proporcionan algo como esto: recompose , Rx.pipe etc., pero, por supuesto, puede escribir su propia función de canalización simple si no desea instalar una biblioteca.

Encuentro esto más fácil de leer que no usar decoradores,

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

Otra razón por la que uso esto es que este patrón se puede encontrar y reemplazar fácilmente una vez que se anuncia la especificación del decorador y se admite correctamente.

@justinfagnani Extrañamente, recibo un error de sintaxis de ESLint cuando intento cambiar extends React.Component<Props, State> a extends withStyles(styles)(React.Component<Props, State>) , por lo que no pude verificar el error de tipo de @G-Rath de la misma manera.

Pero me di cuenta de que si hago algo como lo siguiente, aparece un problema con new (¿tal vez el mismo problema?):

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

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

y el error es:

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

¿Significa esto que el tipo devuelto por Material UI no es un constructor (aunque debería serlo)?

@sagar-sm Pero, ¿obtiene el tipo correcto aumentado por el tipo de interfaz de usuario del material? No parece que lo harás.

Como referencia, me topé con esto porque también estaba tratando de usar withStyles de Material UI como decorador, lo cual no funcionó, así que hice esta pregunta: https://stackoverflow.com/questions/53138167

necesita esto, entonces podemos hacer que la función asíncrona regrese como bluebird

Traté de hacer algo similar. Cajero automático que estoy usando la siguiente solución:

Primero aquí está mi decorador "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>
        }
    }
}

Crear un decorador desde la interfaz

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

Y así es como lo uso:

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

El truco es crear también una interfaz con el mismo nombre que la clase para crear una declaración fusionada.
Aún así, la clase solo se muestra como tipo Test pero las comprobaciones de mecanografiado están funcionando.
Si usara el decorador sin la notación @ y simplemente invóquelo como una función, obtendría el tipo de intersección correcto, pero perdería la capacidad de verificación de tipo dentro de la clase en sí, ya que no puede usar el truco de interfaz más y se ve más feo. P.ej:

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)

¿Qué dices sobre estos enfoques? No es hermoso, pero funciona como una solución alternativa.

¿Hay algún progreso en este tema? Esta parece una característica muy, muy poderosa que desbloquearía muchas posibilidades diferentes. Supongo que este problema está en su mayoría obsoleto en este momento porque los decoradores todavía están experimentando.

Sería genial tener una declaración oficial sobre este @andy-ms @ahejlsberg @sandersn. 🙏

Es probable que no hagamos nada aquí hasta que finalicen los decoradores.

@DanielRosenwasser : ¿qué significa "finalizado" aquí? ¿La Etapa 3 en TC39 califica?

¡No me di cuenta de que habían llegado tan lejos! ¿Cuándo llegaron a la etapa 3? ¿Es eso reciente?

Todavía no lo han hecho; Creo que están listos para avanzar de la Etapa 2 a la Etapa 3 nuevamente en la reunión TC39 de enero.

Puedes estar pendiente de la agenda para más detalles.

Correcto (aunque no puedo confirmar que esté listo para avanzar en enero). Solo preguntaba si la Etapa 3 calificaría cuando lleguen allí.

En mi opinión, la propuesta del decorador todavía tiene muchos problemas serios, por lo que si avanza a la etapa 3 en enero, volverá a cometer el error al igual que la propuesta de campos de clase problemáticos y la propuesta global.

@hax , ¿podrías dar más detalles?
Realmente me gustaría esto y encontrar la falta de comunicación sobre el tema triste y lamentablemente no he oído hablar de los problemas.

@AllNamesRTaken Consulte la lista de problemas de la propuesta del decorador. 😆 Por ejemplo, export antes/después del argumento del decorador es un bloqueador de la etapa 3.

También hay algunos cambios propuestos, como el rediseño de la API, lo que creo que significa que la propuesta no es estable para la etapa 3.

Y también se relacionó con la propuesta de campos de clases problemáticas. Aunque los campos de clase han llegado a la etapa 3, hay demasiados problemas. Un gran problema que me preocupa es que deja muchas cuestiones a los decoradores (por ejemplo, protected) y eso es malo para ambas propuestas.

Tenga en cuenta que no soy delegados de TC39, y algunos delegados de TC39 nunca están de acuerdo con mis comentarios sobre el estado actual de muchos problemas. (Especialmente, tengo una fuerte opinión de que el proceso TC39 actual tiene grandes fallas en muchos temas controvertidos. Posponer algunos problemas para los decoradores nunca resuelve realmente el problema, solo hace que la propuesta del decorador sea más frágil).

Creo que esas cosas se resolverán y la propuesta estará bien, pero preferiría que no discutiéramos el estado de la propuesta aquí.

Creo que una etapa 3 con suficiente confianza o etapa 4 es probablemente donde implementaríamos la nueva propuesta, y luego podríamos analizar este problema.

¡Gracias Daniel! La etapa 3 generalmente implica una fuerte confianza en la 4, así que cruce los dedos para la reunión de enero.

Y gracias por encabezar la discusión de decoradores aquí. Es extraño el nivel de ira y el desprendimiento de bicicletas que ha causado esta función. Nunca había visto algo así 😂

solo para que conste, hubo una solicitud de función sobre lo mismo: https://github.com/Microsoft/TypeScript/issues/8545

bastante molesto, TypeScript admite esta función hasta cierto punto cuando compila desde JavaScript (https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#better-handling-for-namespace-patterns-in -js-archivos):

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

e incluso para el propio código TypeScript, pero solo cuando se trata de funciones (!)(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 ¿ Algún comentario sobre la propuesta de pasar a la Etapa 3? Estoy buscando construir una biblioteca que agregue funciones de prototipo a una clase cuando uso un decorador.

No avanzaron en la última reunión del TC39; pueden estar listos para avanzar nuevamente en la próxima reunión. Sin embargo, no está claro; hay mucho en el aire sobre la propuesta en este momento.

Se recomienda a las personas aquí que estén interesadas que sigan la propuesta en sí , lo que mantendrá el ruido bajo para aquellos de nosotros que vemos el hilo, así como para los mantenedores de TS.

¿alguna actualización en este?

solución alternativa (angular :)) 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
    }
}

¿Qué opinas de esta solución fácil "mientras tanto"?
https://stackoverflow.com/a/55520697/1053872

Mobx-state-tree usa un enfoque similar con su función cast() . No se siente bien pero... Tampoco me molesta demasiado. Es fácil de sacar cuando surge algo mejor.

Me gustaría poder hacer algo similar a esto ❤️
¿No es este el poder que se supone que tienen los decoradores?

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

Pasé bastante tiempo creando una solución alternativa para escribir propiedades de clase decoradas. No creo que sea una solución viable para los escenarios de este hilo, pero puede encontrar algunas partes útiles. Si te interesa, puedes revisar mi publicación en Medium

¿Alguna actualización sobre este tema?

¿Cómo vamos con esto?

¿Qué resultado de este problema, cómo funciona ahora?

Puedes usar mixin-classes para obtener ese efecto.
https://mariusschulz.com/blog/mixin-classes-in-typescript

@Bnaya, que no es en absoluto lo que queremos ahora;).
Los decoradores nos permiten evitar crear clases que nunca pretendemos usar como si estuviéramos haciendo Java de la vieja escuela. Los decoradores permitirían una arquitectura de composición muy limpia y ordenada en la que la clase puede continuar con la funcionalidad central y tener una funcionalidad extra generalizada compuesta. Sí, los mixins podrían hacerlo, pero es curita.

La propuesta del decorador cambió significativamente en los últimos meses: es muy poco probable que el equipo de TS invierta tiempo en la implementación actual que pronto será incompatible™.

Recomiendo ver los siguientes recursos:

Propuesta de decoradores: https://github.com/tc39/proposal-decorators
Hoja de ruta de TypeScript: https://github.com/microsoft/TypeScript/wiki/Roadmap

@Bnaya, que no es en absoluto lo que queremos ahora;).
Los decoradores nos permiten evitar crear clases que nunca pretendemos usar como si estuviéramos haciendo Java de la vieja escuela. Los decoradores permitirían una arquitectura de composición muy limpia y ordenada en la que la clase puede continuar con la funcionalidad central y tener una funcionalidad extra generalizada compuesta. Sí, los mixins podrían hacerlo, pero es curita.

No es una mezcla, sino una clase.
No es la mezcla de cosas de copia mala que
Cuando llegue el operador de la tubería, la sintaxis también será razonable

Puedes usar mixin-classes para obtener ese efecto.

Los mixins de fábrica de clase pueden ser un dolor en TypeScript; incluso si no lo fueran, son muy repetitivos al requerir que envuelva las clases en una función para convertirlas en un mixin, lo que a veces no puede hacer simplemente si está importando clases de terceros.

El siguiente es mucho mejor, más simple, más limpio y funciona con clases de terceros importadas. Lo tengo funcionando bien en JavaScript simple sin otra transpilación que la transpilación del decorador de estilo heredado, pero los class son totalmente nativos 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()

Y la salida en Chrome es:

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

Esto le da completamente lo que hacen los mixins de fábrica de clase, sin todo el repetitivo. Los contenedores de funciones alrededor de sus clases ya no son necesarios.

Sin embargo, como sabe, el decorador no puede cambiar el tipo de la clase definida. 😢

Entonces, por el momento, puedo hacer lo siguiente, con la única salvedad de que los miembros protegidos no son heredables:

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

El problema con la pérdida de miembros protegidos se ilustra con estos dos ejemplos de las implementaciones de multiple (en lo que respecta a los tipos, pero la implementación del tiempo de ejecución se omite por brevedad):

  • ejemplo sin error , porque todas las propiedades en las clases combinadas son públicas.
  • ejemplo con error (en la parte inferior), porque los tipos mapeados ignoran los miembros protegidos, en este caso haciendo que Two.prototype.two no se pueda heredar.

Realmente me gustaría encontrar una forma de mapear tipos que incluyan miembros protegidos.

Esto sería muy útil para mí porque tengo un decorador que permite llamar a una clase como una función.

Esto funciona:

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

Esto no funciona:

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

Se puede realizar una solución alternativa no tan difícil de limpiar cuando aparece esta característica mediante la combinación de declaraciones y un archivo de definición de tipos personalizados.

Teniendo ya esto:

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

Agregue el siguiente archivo (archivo que se eliminará una vez que salga la función):

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

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

Otra posible solución

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

Iniciado en 2015 y aún pendiente. Hay muchos más problemas relacionados e incluso artículos como este https://medium.com/p/caf24aabcb59/responses/show , que intentan mostrar soluciones (que son un poco complicadas, pero útiles)

Es posible que alguien arroje algo de luz sobre esto. ¿Ya se ha considerado siquiera para una discusión interna?

tl; dr: esta característica está atascada en TC39. No puedo culpar a la gente de TS por esto en absoluto. No lo implementarán hasta que sea estándar.

Pero esto es Microsoft, pueden simplemente implementarlo y luego decirle a la gente de estándares: "mira, la gente ya está usando nuestra versión" :trollface:

El problema aquí es que la propuesta de los decoradores ha cambiado _significativamente_ (de maneras incompatibles hacia atrás) desde que TypeScript implementó los decoradores. Esto conducirá a una situación dolorosa cuando los nuevos decoradores se finalicen e implementen, ya que algunas personas confían en el comportamiento y la semántica del decorador actual de TypeScript. Sospecho firmemente que el equipo de TypeScript quiere evitar actuar en este problema porque hacerlo alentaría un mayor uso de los decoradores no estándar actuales, lo que en última instancia haría que la transición a cualquier implementación de decorador nuevo sea aún más dolorosa. Esencialmente, personalmente no veo que esto suceda.

Por cierto, recientemente creé #36348, que es una propuesta para una característica que proporcionaría una solución bastante sólida. Siéntase libre de echarle un vistazo/dar su opinión/salvarlo. 🙂

tl; dr: esta característica está atascada en TC39. No puedo culpar a la gente de TS por esto en absoluto. No lo implementarán hasta que sea estándar.

Dado este hecho, ¿podría ser prudente que el equipo de desarrollo escriba una explicación rápida y una actualización de estado, y bloquee esta conversación hasta que TC39 avance a la etapa requerida?

¿Alguien que lea esto tiene algún peso en el asunto?

En realidad, casi todas las conversaciones relacionadas con los decoradores están suspendidas en el aire. Ejemplo: https://github.com/Microsoft/TypeScript/issues/2607

Será útil obtener algo de claridad sobre el futuro de los decoradores, incluso si se trata de pedirles a los colaboradores que implementen la funcionalidad requerida. Sin una guía clara, el futuro de los decoradores es borroso

Me encantaría que el equipo de TypeScript pudiera asumir un papel proactivo para la propuesta del decorador, similar a cómo ayudaron a lograr el encadenamiento opcional. También me encantaría ayudar, pero aún no he trabajado en el desarrollo del lenguaje y, por lo tanto, no estoy seguro de cuál es la mejor manera de hacerlo :-/

No puedo hablar por todo el equipo y TypeScript como proyecto, pero creo que es poco probable que hagamos un nuevo trabajo de decorador hasta que se resuelva y confirme, dado que la implementación del decorador ya se ha separado una vez de la implementación de TypeScript.

La propuesta aún está en desarrollo activo y apareció en TC39 esta semana https://github.com/tc39/proposal-decorators/issues/305

@orta en este caso, creo que los documentos sobre el decorador de clases deberían actualizarse en ese momento. Se afirma que

Si el decorador de clase devuelve un valor, reemplazará la declaración de clase con la función constructora proporcionada.

Además, el siguiente ejemplo de los documentos parece implicar que el tipo de instancia Greeter tendría la propiedad newProperty , lo cual no es cierto:

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

Creo que vale la pena agregar a los documentos que la interfaz de la clase devuelta no reemplazará a la original.

Interesante, probé un montón de versiones antiguas de TypeScript y no pude encontrar una ocasión en la que funcionara esa muestra de código ( ejemplo de 3.3.3 ), así que sí, estoy de acuerdo.

Hice https://github.com/microsoft/TypeScript-Website/issues/443 : si alguien sabe cómo hacer que classDecorator tenga un tipo de intersección explícito, comente en ese problema

Terminé aquí porque el problema con la documentación mencionada anteriormente me causó confusión. Si bien sería excelente si el Compilador de TS reconociera la firma de tipo del valor devuelto por el decorador, en lugar de ese cambio mayor, acepto que se debe aclarar la documentación.

Para que conste, esta es una versión simplificada de lo que estaba intentando en un formato de los documentos relacionados del 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);
¿Fue útil esta página
0 / 5 - 0 calificaciones