Typescript: Pedido: Mutação do Decorador de Classe

Criado em 20 set. 2015  ·  231Comentários  ·  Fonte: microsoft/TypeScript

Se conseguirmos que isso seja verificado corretamente, teríamos suporte perfeito para mixins sem clichê:

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

Comentários muito úteis

O mesmo seria útil para métodos:

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

O decorador assíncrono deve alterar o tipo de retorno para Promise<any> .

Todos 231 comentários

O mesmo seria útil para métodos:

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

O decorador assíncrono deve alterar o tipo de retorno para Promise<any> .

@Gaelan , isso é exatamente o que estamos precisando aqui! Isso tornaria os mixins naturais de se trabalhar.

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 para isso. Embora eu saiba que isso é difícil de implementar, e provavelmente mais difícil chegar a um acordo sobre a semântica da mutação do decorador.

+1

Se o principal benefício disso for introduzir membros adicionais à assinatura de tipo, você já pode fazer isso com a mesclagem de interface:

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

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

new Foo().foo();

Se o decorador for uma função real que o compilador precisa invocar para alterar imperativamente a classe, isso não parece uma coisa idiomática a se fazer em uma linguagem de tipo seguro, IMHO.

@masaeedu Você conhece alguma solução alternativa para adicionar um membro estático à classe decorada?

@davojan Claro. Aqui está:

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

Também seria útil poder introduzir propriedades _multiple_ em uma classe ao decorar um método (por exemplo, um auxiliar que gera um setter associado para um getter ou algo nesse sentido)

A tipagem react-redux para connect pega um componente e retorna um componente modificado cujas props não incluem as props conectadas recebidas através do redux, mas parece que o TS não reconhece sua definição connect como um decorador devido a este problema. Alguém tem uma solução alternativa?

Acho que a definição do tipo ClassDecorator precisa ser alterada.

Atualmente é declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void; . Talvez possa ser mudado para

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, a nomenclatura é uma merda e não tenho ideia se esse tipo de coisa funcionará (estou apenas tentando converter um aplicativo Babel em texto datilografado e estou acertando isso).

@joyt Você poderia fornecer uma reconstrução do problema no playground? Eu não uso react-redux, mas como mencionei antes, acho que qualquer extensão que você deseja para a forma de um tipo pode ser declarada usando a mesclagem de interface.

@masaeedu aqui está um detalhamento básico das partes móveis.

Basicamente, o decorador fornece vários adereços para o componente React, então o tipo genérico do decorador é um subconjunto do componente decorado, não um superconjunto.

Não tenho certeza se isso é útil, mas tentei montar uma amostra não executável para mostrar os tipos em jogo.

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

Se você quiser um exemplo completo de trabalho, sugiro baixar https://github.com/jaysoo/todomvc-redux-react-typescript ou outro projeto react/redux/typescript de amostra.

De acordo com https://github.com/wycats/javascript-decorators#class -declaration, meu entendimento é que o declare type WrappingClassDecorator = <TFunction extends Function, TDecoratorFunction extends Function>(target: TFunction) => TDecoratorFunction; proposto é inválido.

A especificação diz:

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

é traduzir para:

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

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

Então, se eu entendi corretamente, o seguinte deve ser verdade:

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 o seu exemplo:

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

Estou assumindo que você quer dizer que F retorna uma classe que está em conformidade com X (e não é uma instância de X )? Por exemplo:

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

Nesse caso, as afirmações devem ser:

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

O Foo que está no escopo dessas instruções let foi modificado pelo decorador. O Foo original não está mais acessível. É efetivamente equivalente a:

let Foo = F(class Foo {});

@nevir Sim, você está certo. Obrigado pela clarificação.

Em uma nota lateral, parece que desativar a verificação para invalidar os tipos de classe mutantes é 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:
         }

Mas não tenho conhecimento suficiente para fazer com que o compilador produza as definições de tipo corretas da classe mutante. Tenho o seguinte teste:

testes/casos/conformidade/decoradores/class/decoratorOnClass10.ts

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

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

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

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

Ele gera testes/linhas de base/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

eu estava esperando
>C: typeof C será >C: typeof X | typeof Y

Para aqueles interessados ​​no connect do react-redux como um estudo de caso para este recurso, eu arquivei https://github.com/DefinitelyTyped/DefinitelyTyped/issues/9951 para rastrear o problema em um só lugar.

Eu li todos os comentários sobre esse problema e tenho uma ideia de que a assinatura do decorador não mostra o que ele pode fazer com a classe embrulhada.

Considere este:

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

Deve ser digitado assim:
declare function decorator<T>(target: T): Wrapper<T>;

Mas esta assinatura não nos diz que o decorador adicionou coisas novas ao protótipo do alvo.

Por outro lado, este não nos diz que o decorador realmente retornou um wrapper:
declare function decorator<T>(target: T): T & { someMethod: () => void };

Alguma novidade sobre isso? Isso seria super poderoso para metaprogramação!

Que tal uma abordagem mais simples para este problema? Para uma classe decorada, vinculamos o nome da classe ao valor de retorno do decorador, como um açúcar sintático.

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.

Em termos de implementação, isso introduzirá um símbolo sintético para a classe decorada. E o nome da classe original está vinculado apenas ao escopo do corpo da classe.

@HerringtonDarkholme Acho que seria uma abordagem bem pragmática que forneceria a maior parte da expressividade desejada. Boa ideia!

Eu definitivamente quero ver isso um dia

Costumo escrever uma classe para Angular 2 ou para Aurelia, que se parece com isso:

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

O que eu quero escrever é 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;
    }
  }
}

e então eu poderia aplicá-lo genericamente 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) { }
}

sem perda de segurança tipográfica. Isso seria uma benção para escrever código limpo e expressivo.

Revivendo esta questão.

Agora que o #13743 saiu e o suporte a mixin está na linguagem, esse é um recurso super útil.

@HerringtonDarkholme é menos adequado para este caso, porém, ter que declarar o tipo de retorno do decorador perde alguns recursos dinâmicos ...

@ahejlsberg , @mhegazy Você acha que isso é factível?

Eu tenho outro cenário de uso que não tenho certeza se ainda está coberto por esta conversa, mas provavelmente se enquadra no mesmo guarda-chuva.

Eu gostaria de implementar um decorador de método que alterasse totalmente o tipo do método (não o tipo de retorno ou os parâmetros, mas a função inteira). por exemplo

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 possível em JavaScript, esse não é o problema obviamente, mas em TypeScript eu preciso que o decorador asyncTask mude o tipo do método decorado de () => Promise<void> para AsyncTask<() => Promise<void>> .

Tem certeza de que isso não é possível agora e provavelmente está sob o guarda-chuva desse problema?

@codeandcats seu exemplo é exatamente o mesmo caso de uso para o qual estou aqui!

Oi @ohjames , me perdoe, estou tendo problemas para grocar seu exemplo, alguma chance de você reescrever em algo que funcione como está no playground?

Algum progresso nisso? Eu tive isso na minha cabeça o dia todo, sem saber desse problema, fui implementá-lo apenas para descobrir que o compilador não o detecta. Eu tenho um projeto que poderia usar uma solução de log melhor, então escrevi um singleton rápido para depois expandir em um logger completo que eu anexaria às classes por meio de um decorador como

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

e eu escrevi o código necessário para isso

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

e a propriedade logger está definitivamente presente em tempo de execução, mas infelizmente não é captada pelo compilador.

Eu adoraria ver alguma solução para esse problema, especialmente porque uma construção de tempo de execução como essa deve ser absolutamente capaz de ser representada adequadamente em tempo de compilação.

Acabei me contentando com um decorador de imóveis apenas para me ajudar por enquanto:

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

e anexá-lo a classes como

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

mas isso é muito mais clichê por classe utilizando o registrador do que um simples decorador de classe @loggable . Eu suponho que eu poderia de forma viável typecast como (this as Loggable<this>).logger mas isso também está muito longe do ideal, especialmente depois de fazer isso um punhado de vezes. Ficaria cansativo muito rapidamente.

Eu tive que fazer o TS para um aplicativo inteiro principalmente porque não consegui https://github.com/jeffijoe/mobx-task trabalhando com decoradores. Espero que isso seja abordado em breve. 😄

É muito irritante no ecossistema Angular 2, onde os decoradores e o TypeScript são tratados como cidadãos de primeira classe. No entanto, no minuto em que você tenta adicionar uma propriedade com um decorador, o compilador TypeScript diz que não. Eu teria pensado que a equipe do Angular 2 mostraria algum interesse nessa questão.

@zajrik , você pode realizar o que deseja com mixins de classe que são suportados com tipagem adequada desde o TS 2.2:

Defina seu mixin Loggable assim:

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

e então você pode usá-lo de algumas maneiras. Ou na cláusula extends de uma declaração de classe:

class Foo {
  superProperty: string;
}

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

O TS sabe que as instâncias de LoggableFoo têm superProperty , logger e subProperty :

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

Você também pode usar um mixin como uma expressão que retorna a classe concreta que deseja usar:

const LoggableFoo = Loggable(Foo);

Você _pode_ também usar um mixin de classe como um decorador, mas tem algumas semânticas ligeiramente diferentes, principalmente que é subclasses de sua classe, em vez de permitir que sua classe a subclasse.

Os mixins de classe têm várias vantagens sobre os decoradores, IMO:

  1. Eles criam uma nova superclasse, de modo que a classe à qual você os aplica tenha uma alteração para substituí-los
  2. Eles digitam check agora, sem nenhum recurso adicional do TypeScript
  3. Eles funcionam bem com inferência de tipo - você não precisa digitar o valor de retorno da função mixin
  4. Eles funcionam bem com análise estática, especialmente salto para definição - Saltar para a implementação de logger leva você para a _implementação_ do mixin, não para a interface.

@justinfagnani Eu nem tinha considerado mixins para isso, então obrigado. Vou em frente e escrever um mixin Loggable hoje à noite para tornar minha sintaxe de anexo do Logger um pouco mais agradável. A rota extends Mixin(SuperClass) é a minha preferida, pois é como eu usei mixins até agora desde o lançamento do TS 2.2.

Eu prefiro a ideia de sintaxe de decorador a mixins, no entanto, ainda espero que alguma resolução possa ser obtida para esse problema específico. Ser capaz de criar mixins sem clichê usando decoradores seria um grande benefício para um código mais limpo, na minha opinião.

@zajrik que bom que a sugestão ajudou, espero

Eu ainda não entendo muito bem como os mixins têm mais clichê do que decoradores. Eles são quase idênticos em peso sintático:

Classe Mixin:

class LoggableFoo extends Loggable(Foo) {}

vs Decorador:

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

Na minha opinião, o mixin é muito mais claro sobre sua intenção: está gerando uma superclasse, e as superclasses definem os membros de uma classe, então o mixin provavelmente também está definindo os membros.

Decoradores serão usados ​​para tantas coisas que você não pode assumir que são ou não membros definindo. Pode ser simplesmente registrar a classe para algo ou associar alguns metadados a ela.

Para ser justo, acho que o que @zajrik quer é:

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

O que é inegavelmente, ainda que ligeiramente, menos clichê.

Dito isto, eu amo a solução mixin. Eu continuo esquecendo que mixins são uma coisa.

Se tudo o que você se importa é adicionar propriedades à classe atual, então mixins são basicamente equivalentes a decoradores com um aborrecimento significativo... se sua classe ainda não possui uma superclasse, você precisa criar uma superclasse vazia para usá-las. Além disso, a sintaxe parece mais pesada em geral. Também não está claro se mixins paramétricos são suportados (é extends Mixin(Class, { ... }) permitido).

@justinfagnani na sua lista de razões, os pontos 2-4 são, na verdade, deficiências no TypeScript e não vantagens de mixins. Eles não se aplicam em um mundo JS.

Acho que todos devemos deixar claro que uma solução baseada em mixin para o problema de OPs envolveria adicionar duas classes à cadeia de protótipos, uma das quais é inútil. Isso reflete as diferenças semânticas dos decoradores mixins versus decoradores, mas os mixins dão a você a chance de interceptar a cadeia de classes pai. No entanto, 95% das vezes não é isso que as pessoas querem fazer, elas querem decorar essa aula. Embora os mixins tenham seus usos limitados, acho que promovê-los como uma alternativa aos decoradores e classes de ordem superior é semanticamente inadequado.

Mixins são basicamente equivalentes a decoradores com um aborrecimento significativo... se sua classe ainda não tem uma superclasse você precisa criar uma superclasse vazia para usá-las

Isso não é necessariamente verdade:

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

class Foo extends Mixin() {}

Além disso, a sintaxe parece mais pesada em geral.

Eu simplesmente não vejo como isso é assim, então teremos que discordar.

Também não está claro se mixins paramétricos são suportados (é permitido estender Mixin(Class, { ... })).

Eles são muito. Mixins são apenas funções.

em sua lista de razões, os pontos 2-4 são, na verdade, deficiências no TypeScript e não vantagens de mixins. Eles não se aplicam em um mundo JS.

Este é um problema do TypeScript, então eles se aplicam aqui. No mundo JS, os decoradores ainda não existem.

Acho que todos devemos deixar claro que uma solução baseada em mixin para o problema de OPs envolveria adicionar duas classes à cadeia de protótipos, uma das quais é inútil.

Eu não estou claro onde você consegue dois. É um, assim como o decorador pode fazer, a menos que esteja corrigindo. E qual protótipo é inútil? O aplicativo mixin provavelmente adiciona uma propriedade/método, que não é inútil.

Isso reflete as diferenças semânticas dos decoradores mixins versus decoradores, mas os mixins dão a você a chance de interceptar a cadeia de classes pai. No entanto, 95% das vezes não é isso que as pessoas querem fazer, elas querem decorar essa aula.

Não tenho tanta certeza de que isso seja verdade. Normalmente, ao definir uma classe, você espera que ela esteja na parte inferior da hierarquia de herança, com a capacidade de substituir métodos de superclasse. Os decoradores precisam corrigir a classe, que tem vários problemas, incluindo não trabalhar com super() , ou estendê-la, caso em que a classe decorada não tem a capacidade de substituir a extensão. Isso pode ser útil em alguns casos, como um decorador que substitui todos os métodos definidos da classe para rastreamento de desempenho/depuração, mas está longe do modelo de herança usual.

Embora os mixins tenham seus usos limitados, acho que promovê-los como uma alternativa aos decoradores e classes de ordem superior é semanticamente inadequado.

Quando um desenvolvedor deseja adicionar membros à cadeia de protótipos, os mixins são exatamente semanticamente apropriados. Em todos os casos em que vi alguém querer usar decoradores para mixins, usar mixins de classe realizaria a mesma tarefa, com a semântica que eles realmente esperam dos decoradores, mais flexibilidade devido à propriedade de trabalho com super chamadas e de claro que eles funcionam agora.

Mixins dificilmente são inadequados quando abordam diretamente o caso de uso.

Quando um desenvolvedor deseja adicionar membros à cadeia de protótipos

Esse é exatamente o meu ponto, o OP não quer adicionar nada à cadeia de protótipos. Ele só quer mudar uma única classe e, principalmente, quando as pessoas usam decoradores, elas nem têm uma classe pai diferente de Object. E por algum motivo Mixin(Object) não funciona no TypeScript, então você precisa adicionar uma classe vazia fictícia. Então agora você tem uma cadeia de protótipos de 2 (sem incluir Object) quando não precisa dela. Além disso, há um custo não trivial para adicionar novas classes à cadeia de protótipos.

Quanto à sintaxe, compare Mixin1(Mixin2(Mixin3(Object, { ... }), {... }), {...}) . Os parâmetros para cada mixin estão o mais longe possível da classe de mixin. A sintaxe do decorador é claramente mais legível.

Embora a sintaxe do decorador per-se não digite verificação, você pode simplesmente usar a invocação de função regular para obter o que deseja:

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

É um pouco chato que você tenha que declarar sua classe como const Foo = loggable(class { , mas fora isso tudo funciona.

@ohjames (cc @justinfagnani) você deve ter cuidado ao estender builtins como Object (já que eles atacam o protótipo da sua subclasse em instâncias): https://github.com/Microsoft/TypeScript/wiki/FAQ #why -não-estendendo-embutidos-como-erro-matriz-e-mapa-funciona

@nevir sim, eu já tentei a sugestão de @justinfagnani de usar um mixin com um parâmetro Object padrão no passado com TypeScript 2.2 e tsc rejeita o código.

@ohjames ainda funciona, você só precisa ter cuidado com o protótipo no caso padrão (veja essa entrada de perguntas frequentes).

No entanto, geralmente é mais fácil confiar no comportamento de tslib.__extend quando passado null

Algum plano para focar esse problema na próxima etapa da iteração? Os benefícios desse recurso são extremamente altos em muitas bibliotecas.

Acabei de me deparar com esse problema - isso me força a escrever muito código desnecessário. Resolver esse problema seria uma grande ajuda para qualquer estrutura/biblioteca baseada em decorador.

@TomMarius Como mencionei anteriormente, as classes envolvidas em funções de decorador já digitam check corretamente, você simplesmente não pode usar o açúcar de sintaxe @ . Em vez de fazer:

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

você só precisa fazer:

const Foo = loggable(class { });

Você pode até compor um monte de funções de decorador antes de envolver uma classe nelas. Embora fazer o açúcar de sintaxe funcionar corretamente seja valioso, não parece que isso deva ser um grande ponto de dor como as coisas são.

@masaeedu Realmente o problema não é o suporte de tipo externo, mas interno. Poder usar as propriedades que o decorador adiciona dentro da própria classe sem erros de compilação é o resultado desejado, pelo menos para mim. O exemplo que você forneceu apenas forneceria Foo o tipo logável, mas não permitiria o tipo para a definição de classe em si.

@zajrik Um decorador retorna uma nova classe de uma classe original, mesmo quando você usa a sintaxe @ incorporada. Obviamente, o JS não impõe pureza, então você está livre para alterar a classe original que recebeu, mas isso é incongruente com o uso idiomático do conceito de decorador. Se você está acoplando fortemente a funcionalidade que está adicionando por meio de decoradores aos internos da classe, eles também podem ser propriedades internas.

Você pode me dar um exemplo de um caso de uso para uma API de consumo interno de classe que é adicionada posteriormente por meio de decoradores?

O exemplo do Logger acima é um bom exemplo de um _want_ comum para poder manipular os internos da classe decorada. (E é familiar para pessoas vindas de outras linguagens com decoração de classe; como Python )

Dito isso, a sugestão de mixin de classe de @justinfagnani parece uma boa alternativa para esse caso

Se você quiser definir os componentes internos de uma classe, a maneira estruturada de fazer isso não é corrigir a classe ou definir uma nova subclasse, ambas as quais o TypeScript terá dificuldade em raciocinar no contexto da classe em si, mas apenas definir as coisas na própria classe ou criar uma nova superclasse que tenha as propriedades necessárias, sobre as quais o TypeScript pode raciocinar.

Os decoradores realmente não devem alterar a forma de uma classe de forma que seja visível para a classe ou para a maioria dos consumidores. @masaeedu está aqui.

Embora o que você está dizendo seja verdade, o TypeScript não existe para impor práticas de codificação limpas, mas para digitar corretamente o código JavaScript, e ele falha nesse caso.

@masaeedu O que @zajrik disse. Eu tenho um decorador que declara um "serviço online" que adiciona várias propriedades a uma classe que são usadas na classe. Subclassificar ou implementar uma interface não é uma opção devido à falta de metadados e aplicação de restrições (se você se esforçar para não duplicar o código).

@TomMarius Meu ponto é que está verificando o tipo corretamente. Quando você aplica uma função de decorador a uma classe, a classe não é alterada de forma alguma. Uma nova classe é produzida por meio de alguma transformação da classe original, e somente esta nova classe é garantida para suportar a API introduzida pela função decoradora.

Não sei o que significa "falta de metadados e aplicação de restrições" (talvez um exemplo concreto ajude), mas se sua classe depende explicitamente da API introduzida pelo decorador, ela deve apenas subclassificá-la diretamente por meio do padrão mixin @justinfagnani mostrou , ou injetá-lo através do construtor ou algo assim. A utilidade dos decoradores é que eles permitem que classes fechadas para modificação sejam estendidas para o benefício do código que consome essas classes . Se você tiver liberdade para definir a classe sozinho, use extends .

@masaeedu Se você está desenvolvendo algum tipo de, digamos, uma biblioteca RPC e deseja forçar o usuário a escrever apenas métodos assíncronos, a abordagem baseada em herança força você a duplicar o código (ou não encontrei a maneira correta , talvez - eu ficaria feliz se você me dissesse se você sabe como).

Abordagem baseada em herança
Definição: 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(); } }

Abordagem baseada no decorador
Definição: 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(); } }

Você pode ver claramente a duplicação de código no exemplo de abordagem baseada em herança. Já aconteceu muitas vezes comigo e com meus usuários que eles esqueceram de alterar o parâmetro de tipo quando copiaram e colaram a classe ou começaram a usar "qualquer" como parâmetro de tipo e a biblioteca parou de funcionar para eles; a abordagem baseada em decorador é muito mais amigável ao desenvolvedor.

Depois disso, há outro problema com a abordagem baseada em herança: metadados de reflexão estão faltando agora, então você tem que duplicar o código ainda mais porque você tem que introduzir o decorador service qualquer maneira. O uso agora é: <strong i="22">@service</strong> export default class MyService extends Service<MyService> { async foo() { return this.someMethod(); } } , e isso é simplesmente hostil, não apenas um pouco de inconveniência.

Você é verdade que semanticamente a modificação é feita depois que a classe é definida, no entanto, não há como instanciar a classe não decorada, então não há razão para não suportar adequadamente a mutação da classe, exceto que às vezes permite código impuro (mas às vezes para o bem melhor). Lembre-se que o JavaScript ainda é baseado em protótipos, a sintaxe da classe é apenas um açúcar para cobri-la. O protótipo é mutável e pode ser silenciado pelo decorador, e deve ser digitado corretamente.

Quando você aplica uma função de decorador a uma classe, a classe não é alterada de forma alguma.

Não é verdade, quando você aplica uma função de decorador a uma classe, a classe pode ser alterada, de qualquer forma. Goste você ou não.

@TomMarius Você está tentando explorar a inferência para impor algum contrato, o que é totalmente irrelevante para o argumento aqui. Você deve apenas fazer:

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

Não há absolutamente nenhum requisito para que os internos da classe estejam cientes da função de decoração.

@masaeedu Esse não era o meu ponto. O decorador de serviço/classe de serviço introduz algumas novas propriedades, e elas estão sempre disponíveis para a classe a ser usada, mas o sistema de tipos não reflete isso corretamente. Você disse que eu deveria usar herança para isso, então mostrei porque não posso/não quero.

Editei o exemplo para ficar mais claro.

@masaeedu A propósito, a declaração "Um decorador retorna uma nova classe de uma classe original" está incorreta - cada decorador que mostramos aqui retorna uma classe mutante ou diretamente a classe original, nunca uma nova.

@TomMarius Seu comentário mencionou um problema com "forçar o usuário a escrever apenas métodos assíncronos", que é o problema que tentei abordar no meu comentário. Impor que o usuário tenha seguido o contrato que você espera deve ser feito onde quer que o código do usuário seja passado de volta para a biblioteca e não tem nada a ver com a discussão sobre se os decoradores devem alterar a forma de tipo apresentada aos internos da classe. O problema ortogonal de fornecer uma API para o código do usuário pode ser resolvido com abordagens padrão de herança ou composição.

@ohjames A classe não é alterada apenas pela aplicação de um decorador. JS não impõe pureza, então, obviamente, qualquer declaração em qualquer lugar do seu código pode modificar qualquer outra coisa, mas isso é irrelevante para esta discussão de recursos. Mesmo depois que o recurso for implementado, o TypeScript não o ajudará a rastrear alterações estruturais arbitrárias nos corpos de função.

@masaeedu Você está escolhendo pedaços, mas estou falando sobre o quadro geral. Por favor, revise todos os meus comentários neste tópico - o ponto não está nos problemas individuais, mas em cada problema acontecendo ao mesmo tempo. Acho que expliquei bem o problema com a abordagem baseada em herança - muita e muita duplicação de código.

Para maior clareza, não se trata de "código limpo" para mim. O problema é de praticidade; você não precisa de grandes mudanças no sistema de tipos se tratar @foo da mesma forma que uma aplicação de função. Se você abrir a lata de minhocas tentando introduzir informações de tipo no argumento de uma função a partir de seu tipo de retorno , enquanto ao mesmo tempo interage com a inferência de tipos e todas as outras feras mágicas encontradas em vários cantos do sistema de tipos TypeScript, sinto isso se tornará um grande obstáculo para novos recursos da mesma forma que a sobrecarga é agora.

@TomMarius Seu primeiro comentário neste tópico é sobre código limpo, o que não é relevante. O próximo comentário é sobre esse conceito de serviço online para o qual você forneceu o código de exemplo. A principal reclamação, desde o primeiro parágrafo até o quarto, é sobre como é propenso a erros usar MyService extends Service<MyService> . Tentei mostrar um exemplo de como você pode lidar com isso.

Eu olhei para ele novamente e realmente não consigo ver nada nesse exemplo que ilustre por que os membros da classe decorada precisariam estar cientes do decorador. O que há nessas novas propriedades que você fornece ao usuário que não pode ser realizada com herança padrão? Eu não trabalhei com reflexão, então eu meio que passei por isso, minhas desculpas.

@masaeedu Eu posso fazer isso com herança, mas a herança me força/meus usuários a duplicar massivamente o código, então eu gostaria de ter outra maneira - e eu faço, mas o sistema de tipos não é capaz de refletir corretamente a realidade.

O ponto é que o tipo correto de <strong i="7">@service</strong> class X { } onde service é declarado como <T>(target: T) => T & IService não é X , mas X & IService ; e o problema é que na realidade é verdade mesmo dentro da classe - mesmo que semanticamente não seja verdade.

Outro grande problema que esse problema causa é que, quando você tem uma série de decoradores, cada um com algumas restrições, o sistema de tipos pensa que o destino é sempre a classe original, não a decorada e, portanto, a restrição é inútil.

Eu posso fazer isso com herança, mas a herança me força/meus usuários a duplicar massivamente o código, então eu gostaria de ter outra maneira.

Essa é a parte que não estou entendendo. Seus usuários precisam implementar IService , e você quer garantir que TheirService implements IService também obedeça a algum outro contrato { [P in keyof blablablah] } , e talvez você também queira que eles tenham um { potato: Potato } em seu serviço. Tudo isso é fácil de realizar sem que os membros da classe estejam cientes 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 ""; }
}

Em nenhum dos casos há alguma vitória gigantesca em concisão que só pode ser alcançada apresentando o tipo de retorno de serviceDecorator como o tipo de this para foo . Mais importante, como você propõe criar uma estratégia de digitação ainda mais sólida para isso? O tipo de retorno de serviceDecorator é inferido com base no tipo da classe que você está decorando, que por sua vez agora é digitado como o tipo de retorno do decorador...

Oi @masaeedu , a concisão se torna especialmente valiosa quando você tem mais de um.

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

Essa solução alternativa, no entanto, é apenas uma alternativa aos decoradores de classe. Para decoradores de métodos, atualmente não há soluções e bloqueia tantas implementações interessantes. Decoradores estão em proposta na fase 2 - https://github.com/tc39/proposal-decorators . No entanto, houve muitos casos em que a implementação foi feita muito antes. Eu acho que especialmente os decoradores são um daqueles tijolos importantes que foram realmente importantes, pois já foram usados ​​em muitos frameworks e uma versão muito simples já está implementada no babel/ts. Se esse problema pudesse ser implementado, não perderia seu estado experimental até o lançamento oficial. Mas isso é "experimental" para.

@pietschy Sim, fazer o açúcar de sintaxe @ funcionar corretamente com a verificação de tipo seria bom. No momento, você pode usar a composição de funções para obter uma concisão razoavelmente semelhante:

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

const MyComponent = decorator(class {
});

A discussão anterior é sobre se é uma boa ideia fazer algum tipo de digitação inversa onde o tipo de retorno do decorador é apresentado como o tipo de this para os membros da classe.

Oi @masaeedu , sim eu entendi o contexto da discussão, daí o // do stuff with undo context, component methods etc . Felicidades

realmente precisa tornar Mixins mais fácil.

typescript (javascript) não suporta herança múltipla, então devemos usar Mixins ou Traits.
E agora está me desperdiçando tanto tempo, especialmente quando reconstruo algo.
E devo copiar e colar uma interface com sua "implementação vazia" em todos os lugares. (#371)

--
Não acho que o tipo de retorno do decorador deva ser apresentado na classe.
porque: .... emmm. não sei como descrevê-lo, desculpe pela minha habilidade de inglês ruim ... ( 🤔 pode uma foto existir sem uma moldura?) este trabalho é para interface .

Vou adicionar meu +1 a este! Eu adoraria vê-lo em breve.

@pietschy Se a classe depende de membros adicionados pelos decoradores, ela deve estender o resultado dos decoradores, e não o contrário. Você deveria fazer:

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

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

A alternativa é que o sistema de tipos funcione em algum tipo de loop, onde o argumento da função decoradora é contextualmente digitado por seu tipo de retorno, que é inferido de seu argumento, que é contextualmente digitado por seu tipo de retorno, etc. proposta específica de como isso deve funcionar, não é apenas aguardar a implementação.

Oi @masaeedu , estou confuso porque eu teria que compor meus decoradores e aplicar isso a uma classe base. Tanto quanto eu entendo, o protótipo já foi modificado no momento em que qualquer código de terra do usuário é executado. Por exemplo

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

Portanto, se os métodos definidos pelos decoradores já existirem e puderem ser chamados legitimamente, seria bom se o typescript refletisse isso.

O desejo é que o texto datilografado reflita isso sem impor soluções alternativas. Se houver outros
coisas que os decoradores podem fazer que não podem ser modeladas, então seria pelo menos bom se isso
cenário foi suportado de alguma forma ou forma.

Este tópico está cheio de opiniões sobre o que os decoradores devem fazer, como devem ser usados, como não devem ser usados, etc ad nauseam .

Isto é o que os decoradores realmente fazem:

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

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

que se torna, se você estiver segmentando 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);

com __decorate aplicando cada decorador de baixo para cima, com cada um tendo a opção de retornar uma classe totalmente nova.

Entendo que fazer com que o sistema de tipos dê suporte e reconheça isso pode ser complicado, e entendo que pode demorar muito para dar suporte a isso, mas não podemos todos concordar que o comportamento atual, do exemplo de código original acima que iniciou isso thread, é simplesmente incorreto?

Não importa qual seja a opinião de qualquer desenvolvedor sobre decoradores ou mixins ou composição funcional etc etc etc, isso parece ser um bug bastante claro.


Posso perguntar educadamente se isso está no pipeline para uma versão futura?

TypeScript é simplesmente incrível e eu adoro isso; isso, no entanto, parece ser uma das poucas (únicas?) peças que está simplesmente quebrada, e estou ansioso para vê-la consertada :)

@arackaf É bem entendido o que os decoradores realmente fazem, e supondo que você não se importe com os tipos que o TypeScript emite já suporta os decoradores. Todo o debate nesta edição é sobre como as classes decoradas devem ser apresentadas no sistema de tipos.

Eu concordo que new Foo().foo ser um erro de tipo é um bug, e facilmente corrigido. Eu não concordo que return this.foo; ser um erro de tipo seja um bug. Se alguma coisa, você está solicitando um recurso no sistema de tipos que até agora ninguém nesta edição ainda especificou. Se você tem algum mecanismo em mente pelo qual o tipo de this deve ser transformado por um decorador aplicado à classe que o contém, você precisa sugerir esse mecanismo explicitamente .

Esse mecanismo não é tão trivial quanto você poderia esperar, porque em quase todos os casos o tipo de retorno do decorador é inferido do próprio tipo que você está propondo transformar usando o tipo de retorno do decorador. Se o decorador addMethod pegar uma classe do tipo new () => T e produzir new() => T & { hello(): void } , não faz sentido sugerir que T seja T & { hello(): void } . Dentro do corpo do decorador, é válido para mim fazer referência a super.hello ?

Isso é especialmente pertinente porque não estou restrito a fazer return class extends ClassIWasPassed { ... } no corpo do decorador, posso fazer o que quiser; tipos de subtração, tipos mapeados, uniões, todos eles são um jogo justo. Qualquer tipo resultante deve funcionar bem com esse loop de inferência que você está sugerindo. Como ilustração do problema, diga-me o que deve acontecer neste exemplo:

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

Você não pode simplesmente descartar o problema como sendo "complicado", alguém precisa realmente propor como isso deve funcionar.

Não é um erro - você está passando ao Logger uma classe que está em conformidade com o Greeter; no entanto, Foo é então modificado pelo decorador para ser algo completamente diferente, o que não estende mais o Greeter. Portanto, deve ser válido para o decorador de logger tratar Foo como Greeter, mas inválido para qualquer outra pessoa, já que Foo é reatribuído a algo completamente diferente pelo decorador.

Tenho certeza de que implementar isso seria visciously difícil. É possível algum subconjunto razoável, para os casos comuns, como o listado no início deste tópico, com soluções alternativas, como mesclagem de interface, sendo alternativas para casos complicados como esse?

@arackaf

no entanto, Foo é então mutado pelo decorador para ser algo completamente diferente

Não há uma definição precisa para o que Foo é em primeiro lugar. Lembre-se de que todo o nosso ponto de discórdia é se os membros de Foo devem poder acessar (e, portanto, retornar como parte da API pública), o decorador introduziu membros de this . O que Foo é é definido pelo que o decorador retorna, e o que o decorador retorna é definido pelo que Foo é. Eles são inseparavelmente unidos.

Tenho certeza de que implementar isso seria extremamente difícil

Ainda é prematuro falar em complexidade de implementação, quando ainda não temos uma proposta sólida de como o recurso deve funcionar. Desculpe por insistir nisso, mas realmente precisamos que alguém proponha um mecanismo concreto para "o que acontece com this quando eu aplico essa sequência de decoradores a essa classe". Podemos então conectar vários conceitos do TypeScript em diferentes partes e ver se o resultado faz sentido. Só então faz sentido discutir a complexidade da implementação.

A saída do decorador atual que colei acima não é compatível com as especificações? Eu assumi que era pelo que eu vi.

Supondo que seja, Foo tem um significado bastante preciso a cada passo do caminho. Eu esperaria que o TS me permitisse usar this (dentro de métodos Foo e em instâncias de Foo) com base no que o último decorador retorna, com o TS me forçando a adicionar anotações de tipo ao longo do caminho, conforme necessário, se o decorador funções não são suficientemente analisáveis.

@arackaf Não há "passos do caminho"; estamos preocupados apenas com o tipo final de this para um determinado snippet de código imutável. Você precisa descrever, pelo menos em alto nível, o que você espera que o tipo de this seja para membros de uma classe X decorada com funções de decorador f1...fn , em termos das assinaturas de tipo de X e f1...fn . Você pode ter quantas etapas no processo desejar. Até agora, ninguém fez isso. Eu tenho adivinhado que as pessoas querem dizer que o tipo de retorno do decorador deve ser apresentado como o tipo de this , mas pelo que sei, posso estar totalmente errado.

Se sua proposta é fazer com que os tipos reflitam mecanicamente o que está acontecendo com os valores na saída transpilada, você acaba com o que estou propondo em vez do que você está propondo: ou seja new Foo().hello() está bom, mas this.hello() não é. Em nenhum momento nesse exemplo a classe original que você está decorando ganha um método hello . Apenas o resultado de __decorate([addMethod], Foo) (que é então atribuído a Foo ) tem um método hello .

Eu tenho adivinhado que as pessoas querem dizer que o tipo de retorno do decorador deve ser apresentado como o tipo deste

Oh, desculpe, sim, isso mesmo. Exatamente isso. Ponto final. Porque é isso que os decoradores FAZEM. Se o último decorador na linha retornar alguma classe completamente nova, então é isso que Foo é.

Em outras palavras:

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

Foo é o que no mundo c diz que é. Se c é uma função que retorna any então, eu não sei - talvez apenas volte para o Foo original? Isso parece uma abordagem razoável e compatível com versões anteriores.

mas se c retornar algum novo tipo X , então eu absolutamente esperaria que Foo respeitasse isso.

Estou esquecendo de algo?


esclarecendo melhor, se

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 

Estou esquecendo de algo?

@arackaf Sim, o que está faltando nessa abordagem ingênua é que um decorador seja livre para retornar tipos arbitrários, sem restrição de que o resultado seja compatível com o tipo de Foo .

Há uma série de absurdos que você pode produzir com isso. Digamos que eu suspenda minha objeção sobre a circularidade de digitar this como resultado de c , que é determinado pelo tipo de this , que é determinado pelo resultado de c , etc. Isso ainda não funciona. Aqui está outro exemplo sem 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(); }
}

Para este caso, você é forçado a produzir um erro para código totalmente benigno ou ajustar a proposição de que this seja exatamente idêntico ao tipo de retorno do decorador.

Não tenho certeza se vejo o problema. @logger optou por retornar uma classe totalmente nova, sem nenhum método foo e com um método hello() totalmente novo, que por acaso faz referência ao original, agora inacessível Foo .

new Foo().foo()

não é mais válido; ele irá produzir um erro de tempo de execução. Estou apenas dizendo que também deve produzir um erro em tempo de compilação.

Dito isso, se analisar estaticamente tudo isso for muito difícil, seria totalmente razoável imo nos forçar a adicionar anotações de tipo explícito a logger para significar exatamente o que está sendo retornado. E se nenhuma anotação de tipo estiver presente, eu diria apenas reverter para assumir que Foo é retornado. Isso deve mantê-lo compatível com versões anteriores.

@arackaf Não há problema com o código em termos de digitação ou avaliação de tempo de execução. Posso chamar new Foo().hello() , que chamará internamente o hello da classe decorada, que chamará o bar da classe decorada. Não é um erro invocar bar dentro da classe original.

Você pode experimentá-lo executando este exemplo completo no playground:

// Code from previous snippet...

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

Claro - mas eu disse que era um erro invocar

new Foo().foo()

Não há problema com o código em termos de digitação ou avaliação de tempo de execução. Posso chamar new Foo().hello(), que chamará internamente o hello da classe decorada, que chamará a barra da classe decorada

Mas deve ser um erro dizer

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

já que o método hello de Foo agora retorna void, de acordo com a classe que Logger retorna.

Claro - mas eu disse que era um erro invocar new Foo().foo()

@arackaf Mas isso não importa. Eu não invoquei new Foo().foo() . Invoquei this.foo() e recebi um erro de tipo, embora meu código funcione bem em tempo de execução.

Mas deve ser um erro dizer let s : string = new Foo().hello()

Novamente, isso é irrelevante. Não estou dizendo que o tipo final de Foo.prototype.hello deve ser () => string (concordo que deve ser () => void ). Estou reclamando sobre a invocação válida this.bar() estar errada, porque você transplantou cirurgicamente um tipo onde não faz sentido transplantá-lo.

Há dois Foo aqui. Quando voce diz

class Foo { 
}

Foo é uma ligação imutável DENTRO da classe e uma ligação mutável fora da classe. Então isso funciona perfeitamente como você pode verificar em um jsbin

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

let F = Foo;

Foo = null;

new F().hello();

Seu exemplo acima faz coisas semelhantes; ele captura uma referência à classe original antes que a ligação externa seja alterada. Ainda não tenho certeza do que você está querendo dizer.

this.foo(); é perfeitamente válido, e eu não esperaria um erro de tipo (eu também não culparia o pessoal do TS se eu precisasse de alguma referência, pois tenho certeza que rastrear isso seria difícil)

this.foo(); é perfeitamente válido, e eu não esperaria um erro de tipo

Bom, então concordamos, mas isso significa que agora você precisa qualificar ou rejeitar a proposta de que this seja o tipo de instância do que o decorador retornar. Se você acha que não deveria ser um erro de tipo, o que deveria ser this em vez de { hello(): void } no meu exemplo?

this depende do que foi instanciado.

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

Podemos, por favor, ficar com um exemplo concreto? Isso tornaria as coisas muito mais fáceis para mim entender. No trecho a seguir:

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

Qual é o tipo de this ? Se for { hello(): void } , obterei um erro de tipo, porque foo não é membro de { hello(): void } . Se não for { hello(): void } , então this não é simplesmente o tipo de instância do tipo de retorno do decorador, e você precisa explicar qualquer lógica alternativa que esteja usando para chegar ao tipo de this .

EDIT: Esqueci de adicionar o decorador em Foo . Fixo.

this , onde você tem as setas, é obviamente uma instância do Foo original. Não há erro de tipo.

Ah - eu vejo seu ponto agora; mas ainda não vejo onde está o problema. this.foo() DENTRO do Foo original não é um erro de tipo - isso é válido para a classe (agora inacessível) que costumava ser vinculada ao identificador Foo .

É uma idiossincrasia, curiosidades divertidas, mas não vejo por que isso deve impedir o TS de lidar com decoradores de classe mutantes.

@arackaf Você não está respondendo à pergunta. Qual é, especificamente, o tipo de this ? Você não pode simplesmente responder infinitamente " this é Foo e Foo é this ". Quais membros this tem? Se tiver membros além hello(): void , que lógica está sendo usada para determiná-los?

Quando você diz " this.foo() DENTRO do Foo original não é um erro de tipo", você ainda precisa responder à pergunta: qual é o tipo estrutural de this modo que não seja um erro de tipo fazer this.foo() ?

Além disso, a classe original não é "inalcançável". Cada função definida nesse trecho de código é exercida ativamente em tempo de execução e tudo funciona sem problemas. Por favor, execute o link do playground que eu forneci e olhe para o console. O decorador retorna uma nova classe na qual o método hello delega ao método hello da classe decorada, que por sua vez delega ao método foo da classe decorada.

É uma idiossincrasia, curiosidades divertidas, mas não vejo por que isso deve impedir o TS de lidar com decoradores de classe mutantes.

Não há "trivia" no sistema de tipos. Você não receberá um erro de tipo TSC-1234 "garoto travesso, você não pode fazer isso" porque o caso de uso é muito específico. Se um recurso está causando a quebra de código perfeitamente normal de maneiras surpreendentes, o recurso precisa ser repensado.

Você não receberá um erro de tipo TSC-1234 "garoto travesso, você não pode fazer isso" porque o caso de uso é muito específico.

Isso é EXATAMENTE o que recebo quando tento usar um método que foi adicionado a uma definição de classe por um decorador. Atualmente, tenho que contornar isso adicionando uma definição à classe ou usando a mesclagem de interface, lançando como any etc.

Eu respondi todas as perguntas sobre o que é this e onde.

O simples fato da questão é que o significado de this muda com base em onde você está.

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"

quando você diz new Class() - Class está apontando para o Foo original e, da perspectiva do TypeScript, ele teria acesso a hello(): string pois é assim que Class é digitado (estende Greeter). Em tempo de execução, você instanciará uma instância do Foo original.

new LoggableFoo().hello() chama um método void que chama um método que de outra forma é inacessível, digitado por meio de Greeter.

Se você tivesse feito

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

então Foo agora é uma classe com apenas hello(): void e new Foo().foo() deve ser um erro de tipo.

E, novamente, hello() { return this.foo(); } não é um TypeError - por que seria? Só porque essa definição de classe não é mais acessível não torna esse método inválido.

Eu não espero que o TypeScript consiga qualquer um desses casos extremos perfeitos; seria bastante compreensível ter que adicionar anotações de tipo aqui e ali, como sempre. Mas nenhum desses exemplos mostra por que @logger não pode mudar fundamentalmente o que Foo está vinculado.

Se logger é uma função que retorna uma nova classe, então é ISSO que Foo agora referencia.

Eu respondi todas as perguntas sobre o que é isso e onde.
O simples fato da questão é que o significado de this muda com base em onde você está.

Isso é realmente frustrante. Tudo bem, muda, é Foo , é uma ligação estática, etc. etc. etc. Qual é a assinatura de tipo? Quais membros this tem? Você está falando sobre tudo sob o sol, quando tudo que eu preciso de você é uma assinatura de tipo simples para o que this está dentro hello .

new LoggableFoo().hello() chama um método void que chama um método que de outra forma é inacessível, digitado por meio de Greeter.

Isso não é o mesmo que inacessível. Todo método alcançável é "inalcançável de outra forma" quando você desconta os caminhos pelos quais ele é alcançável.

Se você tivesse feito:

Mas isso é literalmente o que eu tenho feito. Esse é exatamente o meu trecho de código, colado novamente, prefaciado com uma explicação do exemplo que construí para você. Eu até passei por um verificador de diferenças para ter certeza de que não estava tomando pílulas malucas, e a única diferença é o comentário que você removeu.

E, novamente, hello() { return this.foo(); } não é um TypeError - por que seria?

Porque você (e outros aqui) quer que o tipo de this seja o tipo de retorno instanciado do decorador, que é { hello(): void } (observe a ausência de um membro foo ). Se você quiser que os membros de Foo possam ver this como o tipo de retorno do decorador, o tipo de this dentro hello será { hello(): void } . Se for { hello(): void } , recebo um erro de tipo. Se eu receber um erro de tipo, fico triste, porque meu código funciona bem.

Se você disser que não é um erro de tipo, você está abandonando seu próprio esquema para fornecer o tipo de this através do tipo de retorno do decorador. O tipo de this é então { hello(): string; bar(): string } , independentemente do que o decorador retorna. Você pode ter algum esquema alternativo para produzir o tipo de this que evite esse problema, mas você precisa especificar qual é.

Você parece não entender que, após a execução dos decoradores, Foo pode referenciar algo totalmente separado do que foi definido 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

Suponho que você encontre alguma estranheza em this ser algo diferente dentro de Foo acima, do que em instâncias que são posteriormente criadas a partir do que Foo eventualmente é (depois que os decoradores são executados)?

Não tenho certeza do que dizer; é assim que os decoradores trabalham. Meu único argumento é que o TypeScript deve corresponder mais ao que está acontecendo.

Em outras palavras, as assinaturas de tipo dentro do Foo (original) serão diferentes do que Foo é/produz depois que os decoradores forem executados.

Para emprestar uma analogia de outra linguagem, dentro do Foo decorado, isso estaria referenciando o equivalente a um tipo anônimo C# - um tipo totalmente real, que é válido, mas não pode ser referenciado diretamente. Pareceria estranho obter os erros e não erros descritos acima, mas, novamente, é assim que funciona. Os decoradores nos dão um tremendo poder para fazer coisas bizarras como essa.

Suponho que você encontre alguma estranheza em ser algo diferente dentro de Foo acima, do que em instâncias que são posteriormente criadas a partir do que Foo eventualmente é (depois que os decoradores são executados)?

Não. Não encontro nenhuma estranheza nisso, porque é exatamente o que eu estava propondo há 200 comentários. Você leu alguma das discussões anteriores?

O trecho que você postou é totalmente incontroverso. As pessoas com quem eu estava discordando, e cuja ajuda você pulou para além disso , querem o seguinte:

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

Discordo que seja possível fazer isso de forma sólida e tenho tentado desesperadamente descobrir como essa sugestão funcionaria. Apesar de meus melhores esforços, não consigo extrair uma lista completa dos membros que this deve ter, ou como essa lista seria construída sob a proposta.

Em outras palavras, as assinaturas de tipo dentro do Foo (original) serão diferentes do que Foo é/produz depois que os decoradores forem executados.

Então, por que você está discutindo comigo? Eu disse: "Concordo que new Foo().foo sendo um erro de tipo é um bug, e facilmente consertado. Não concordo que retorne this.foo; ser um erro de tipo é um bug". Analogamente, no seu exemplo, concordo que new Foo().x() ser um erro de tipo é um bug, mas this.x() ser um erro de tipo não é.

Você vê como há dois comentários no snippet no topo desta página?

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

^ Esse é o que eu acho problemático. Ou você concorda que o tipo de retorno do decorador não deve ser apresentado em this , e apenas em new Foo() , nesse caso nós dois não estamos discutindo sobre nada. Ou você não concorda e deseja esse recurso também, caso em que o snippet em seu comentário anterior é irrelevante.

Finalmente entendi seu ponto. Foi extremamente difícil para mim obter isso do seu código Greeter, mas estou rastreando agora; obrigado por ser tão paciente.

Eu diria que a única solução sensata seria que Foo ( dentro de Foo) suportasse o tipo union - derp eu quis dizer interseção - do Foo original e o que quer que o último decorador retorne. Você tem que suportar o Foo original para exemplos malucos como o seu Greeter, e você definitivamente precisa suportar o que o último decorador retornar, pois esse é o objetivo de usar decoradores (de acordo com os muitos comentários acima).

Então, sim, do meu exemplo mais recente, dentro de Foo x, y, z, a, b, c funcionariam. Se houver duas versões de a , suporte ambas.

@arackaf np, obrigado por sua paciência também. Meus exemplos não foram os mais claros porque estou apenas postando tudo o que posso fazer no playground que demonstra como está quebrado. É difícil para mim pensar nisso sistematicamente.

Eu diria que a única solução sensata seria que Foo (dentro de Foo) suportasse a união de tipos do Foo original e o que quer que o último decorador retorne.

Ok, incrível, então estamos entrando em detalhes agora. Corrija-me se estiver errado, mas quando você diz "união" você quer dizer que deve ter os membros do tipo de ambos, ou seja, deve ser A & B . Então queremos que this seja typeof(new OriginalClass()) & typeof(new (decorators(OriginalClass))) , onde decorators é a assinatura de tipo composta de todos os decoradores . Em linguagem simples, queremos que this seja a interseção do tipo instanciado da "classe original" e o tipo instanciado da "classe original" passado por todos os decoradores.

Há dois problemas com isso. Uma é que em casos como o meu exemplo, isso apenas permite que você acesse membros inexistentes. Eu poderia adicionar vários membros no decorador, mas se eu tentasse acessá-los na minha classe usando this.newMethod() , ele vomitaria em tempo de execução. newMethod é adicionado apenas à classe retornada da função decoradora, os membros da classe original não têm acesso a ela (a menos que eu use especificamente o padrão return class extends OriginalClass { newMethod() { } } ).

O outro problema é que "classe original" não é um conceito bem definido. Se eu puder acessar membros decorados de this , também posso usá-los como parte das instruções de retorno e, portanto, eles podem fazer parte da API pública da "classe original". Estou meio que acenando aqui, e estou um pouco esgotado demais para pensar em exemplos concretos, mas acho que se pensarmos muito sobre isso, poderíamos encontrar exemplos sem sentido. Talvez você possa contornar isso encontrando uma maneira de segregar membros que não retornam coisas que acessaram de this ou pelo menos para os quais o tipo de retorno não é inferido como resultado do retorno de this.something() .

@masaeedu sim, eu me corrigi na coisa da união / interseção antes da sua resposta. É contra-intuitivo para alguém novo no TS.

Entendido no resto. Honestamente, os decoradores geralmente não retornarão um tipo completamente diferente, eles geralmente apenas aumentarão o tipo que foi passado, então a coisa da interseção "simplesmente funcionará" com segurança na maioria das vezes.

Eu diria que os erros de tempo de execução de que você fala seriam raros e o resultado de algumas decisões de desenvolvimento propositalmente ruins. Não tenho certeza se você realmente precisa se preocupar com isso, mas, se realmente for um problema, eu diria que usar o que o último decorador retornou seria um segundo lugar decente (então sim, uma classe pode ver um TypeError por tentando usar um método que define por si mesmo - não é o ideal, mas ainda é um preço que vale a pena pagar pelo trabalho dos decoradores).

Mas, na verdade, acho que os erros de tempo de execução em que você está pensando não valem a pena ser evitados, certamente às custas dos decoradores funcionarem corretamente. Além disso, é muito fácil produzir erros de tempo de execução no TS se você for descuidado ou bobo.

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

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

Em relação à sua segunda objeção

Eu também posso usá-los como parte das declarações de retorno e, portanto, eles podem fazer parte da API pública da "classe original"

Receio não ver nenhum problema nisso. Eu quero que qualquer coisa adicionada à classe através de um decorador seja absolutamente um cidadão de primeira classe. Estou curioso para saber quais seriam os possíveis problemas.

Eu acho que uma outra boa opção seria implementar isso apenas parcialmente.

Atualmente, as classes são sempre digitadas conforme são definidas. Portanto, dentro de Foo, isso é baseado em como foo é definido, independentemente dos decoradores. Seria uma grande, enorme melhoria "apenas" estender isso para algum subconjunto útil de casos de uso do decorador (ou seja, os mais comuns)

E se você permitir que a classe seja estendida (da perspectiva de this dentro da classe) se e somente se o decorador retornar algo que estenda o original, ou seja, para

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

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

Faça com que funcione e forneça suporte de primeira classe para blah e qualquer outra coisa adicionada pelo decorador. E para casos de uso que fazem coisas malucas como retornar uma classe totalmente nova (como seu Greeter), apenas continue o comportamento atual e ignore o que o decorador está fazendo.


Aliás, independentemente do que você escolher, como eu anotaria isso? Isso pode ser anotado atualmente? eu tentei

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

assim como muitas variações desse tema, mas o TypeScript não tinha nada disso :)

@arackaf Um decorador é apenas uma função. No caso geral, você o obterá de um arquivo .d.ts em algum lugar e não tem ideia de como ele é implementado. Você não sabe se a implementação está retornando uma classe totalmente nova, adicionando/subtraindo/substituindo membros no protótipo da classe original e retornando-a ou estendendo a classe original. Tudo o que você tem disponível é o tipo de retorno estrutural da função.

Se você quiser de alguma forma vincular os decoradores à herança de classes, primeiro você precisa propor um conceito de linguagem separado para JS. A maneira como os decoradores trabalham hoje não justifica a mutação this no caso geral. Como exemplo, eu pessoalmente sempre prefiro composição sobre herança, e sempre faria:

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

Este não é um experimento acadêmico maluco, é uma abordagem padrão do pântano para fazer mixins, e rompe com o exemplo que dei a você. Na verdade, quase tudo, exceto return class extends Class quebra com o exemplo que eu dei a você, e em muitos casos as coisas vão se equilibrar com return class extends Class .

Você terá que passar por todos os tipos de arcos e contorções para fazer isso funcionar, e o sistema de tipos lutará com você a cada passo do caminho, porque o que você está fazendo é sem sentido no caso geral. Mais importante, uma vez que você o implemente, todos terão que manobrar acrobaticamente em torno desse canto sem sentido do sistema de tipos sempre que estiverem tentando implementar algum outro conceito complexo (mas sólido).

Só porque você tem um caso de uso que você acha importante (e eu tentei várias vezes neste tópico demonstrar maneiras alternativas de expressar o que você quer no sistema de tipos existente), não significa que a única coisa certa fazer é seguir em frente com sua abordagem sugerida a qualquer custo. Você pode mesclar interfaces arbitrárias para sua classe, incluindo os tipos de retorno de funções de decorador, então não é impossível chegar a lugar algum se você insistir em usar decoradores do jeito que você os está usando.

@arackaf isso:

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

Deve funcionar bem na cláusula extends:

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

Com semântica muito mais fácil e já definida no sistema de tipos. Isso já funciona. Que problema você está tentando resolver por subclasses da classe declarada, em vez de criar uma superclasse para ela?

@masaeedu

A forma como os decoradores trabalham hoje não justifica a mutação no caso geral.

O principal uso para decoradores (classe) é absolutamente, positivamente, alterar o valor de this dentro da classe, de uma forma ou de outra. O @connect @observer MobX recebem uma aula e lançam uma NOVA versão da classe original, com recursos adicionais. Nesses casos específicos, não acho que a estrutura real de this mude (apenas a estrutura de this.props ), mas isso é irrelevante.

É um caso de uso bastante comum (como você pode ver nos comentários acima) usar um decorador para alterar uma classe de alguma forma). Para melhor ou pior, as pessoas tendem a não gostar das alternativas sintáticas

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

em oposição a

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

Agora, se um decorador faz

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

ou

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

não deveria importar. De uma forma ou de outra, o caso de uso pelo qual muitos estão realmente clamando é poder dizer ao TypeScript, por quaisquer anotações necessárias, não importa o quão inconveniente para nós, que function c leva uma classe C in e retorna uma classe que tem a estrutura C & {blah(): void} .

Isso é o que muitos de nós estão usando ativamente decoradores para hoje. Nós realmente queremos integrar um subconjunto útil desse comportamento no sistema de tipos.

É facilmente demonstrado que os decoradores podem fazer coisas bizarras que seriam impossíveis para o sistema de tipos rastrear. Multar! Mas deve haver alguma maneira de anotar que

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

é válido. Não sei se exigirá uma nova sintaxe de anotação de tipo para c , ou se a sintaxe de anotação de tipo existente pode ser pressionada em c para fazer isso, mas apenas corrigindo o uso geral case (e deixar as bordas como estão, sem nenhum efeito acontecendo na classe original) seria um grande benefício para os usuários do TS.

@justinfagnani para isso

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

produz

O tipo 'typeof (classe anônima)' não pode ser atribuído ao tipo 'T & (new () => { blah(): void; })'.
O tipo 'typeof (classe anônima)' não pode ser atribuído ao tipo 'T'.

no parque TS. Não tenho certeza se algumas opções estão erradas.

Que problema você está tentando resolver por subclasses da classe declarada, em vez de criar uma superclasse para ela?

Estou apenas tentando usar decoradores. Estou feliz em usá-los em JS há mais de um ano - estou ansioso para que o TS se atualize. E eu certamente não sou casado com subclasses. Às vezes, o decorador apenas altera o protótipo da classe original para adicionar propriedades ou métodos. De qualquer forma, o objetivo é dizer ao TypeScript que

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

resultará em uma classe com novos membros, criada por c .

@arackaf Estamos perdendo a sincronia novamente. O debate não é sobre isso:

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

contra isso:

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

Espero que você seja capaz de usar o primeiro e ainda tenha a digitação adequada de new Foo() . A discussão é sobre isso:

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

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

O último funcionará sem quebrar o sistema de tipos. O primeiro é problemático de várias maneiras que já ilustrei.

@masaeedu realmente não há casos de uso restritos nos quais o primeiro possa funcionar?

Para ser preciso: não há anotações que o TypeScript possa nos dizer para adicionar a a , b e c do seu exemplo acima para que o sistema de tipos trate o primeiro indistinguivelmente deste último?

Isso seria um avanço matador para o TypeScript.

@arackaf desculpe, você precisa fazer com que o parâmetro type se refira ao tipo de construtor, não ao tipo de instância:

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

Estou apenas tentando usar decoradores.

Isso não é realmente um problema que você está tentando resolver, e é uma tautologia. É como Q: "o que está tentando fazer com esse martelo?" R: "Basta usar o martelo".

Existe um caso real que você tem em mente onde a geração de uma nova superclasse com a extensão pretendida não funciona? Como o TypeScript suporta isso agora e, como eu disse, é muito mais fácil raciocinar sobre o tipo da classe de dentro da classe nesse caso.

Eu olhei para a anotação @observer do Mobx e parece que ela não muda a forma da classe (e poderia facilmente ser um decorador de método em vez de um decorador de classe, pois apenas envolve render() ).

@connect é na verdade um ótimo exemplo das complexidades de digitar decoradores corretamente, porque @connect parece nem mesmo retornar uma subclasse da classe decorada, mas uma classe inteiramente nova que envolve a classe decorada, ainda as estáticas são copiadas, então o resultado descarta a interface do lado da instância e preserva o lado estático que o TS nem verifica na maioria dos casos. Parece que @connect realmente não deveria ser um decorador e connect deveria ser usado apenas como um HOC, porque como um decorador, é terrivelmente confuso.

Eu felizmente os uso em JS há mais de um ano

Bem... você não tem realmente. JS não tem decoradores. Você provavelmente está usando uma proposta anterior extinta em particular, que foi implementada de maneira um pouco diferente no Babel e no TypeScript. Em outros locais, estou defendendo que os decoradores continuem com o processo de padronização, mas o progresso diminuiu e acho que ainda não é uma certeza. Muitos argumentam que eles não devem ser adicionados, ou apenas anotações, então a semântica ainda pode estar no ar.

Uma das reclamações contra os decoradores é exatamente que eles podem mudar a forma da classe, dificultando o raciocínio estático, e algumas propostas foram discutidas pelo menos para limitar a capacidade dos decoradores de fazer isso, embora eu ache que isso foi antes do proposta mais recente tem linguagem de especificação. Pessoalmente, defendo que decoradores bem comportados _não devem_ alterar a forma da classe, de modo que, para fins de análise estática, eles possam ser ignorados. Lembre-se de que o JS padrão não tem um sistema de tipos para contar à análise o que uma implementação de função está fazendo.

  • Estou ansioso para que TS alcance.

Não vejo como o TypeScript está atrasado aqui. Pelo menos, atrás de quê? Decoradores trabalham. Existe outro JS tipado onde as classes se comportam da maneira que você deseja?

Os decoradores agora estão no estágio 2 e são usados ​​em praticamente todos os frameworks JS.

Quanto a @connect , o react-redux está com cerca de 2,5 milhões de downloads por mês; é incrivelmente arrogante ouvir que o design está de alguma forma "errado" porque você o teria implementado de forma diferente.

As tentativas atuais de usar métodos introduzidos por um decorador resultam em erros de tipo no TypeScript, exigindo soluções alternativas como mesclagem de interface, adicionando manualmente a anotação para os métodos adicionados ou simplesmente não usando decoradores para alterar classes (como se alguém precisasse ser informado de que poderíamos simplesmente não use decoradores).

Existe realmente, realmente, nenhuma maneira de informar manualmente e meticulosamente ao compilador TS que um decorador está alterando a forma de uma classe? Talvez isso seja loucura, mas o TS não poderia simplesmente enviar um novo decoradortipo que poderíamos usar

let c : decorador

E SE (e somente se) decoradores desse tipo forem aplicados a uma classe, TS considerará a classe como sendo do tipo Input & { blah() } ?

Literalmente todo mundo sabe que não podemos usar decoradores. A maioria também sabe que um grupo vocal não gosta de decoradores. Este post é sobre como o TS poderia, de alguma forma, fazer seu sistema de tipos entender que um decorador está mudando uma definição de classe. Isso seria um grande benefício para muitas pessoas.

Para ser preciso: não há anotações que o TypeScript possa nos dizer para adicionar a a, b e c do seu exemplo acima para que o sistema de tipos trate o primeiro de forma indistinguível do último?

Sim, é muito fácil declarar qualquer forma para Foo que você quiser, você só precisa usar a mesclagem de interface. Você só precisa fazer:

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

No momento, você provavelmente precisa esperar que qualquer biblioteca de decorador que esteja usando exporte tipos parametrizados correspondentes ao que eles estão retornando, ou você mesmo precisará declarar os membros que estão adicionando. Se obtivermos #6606, você pode simplesmente fazer interface Foo extends typeof(a(b(c(Foo)))) .

As tentativas atuais de usar métodos introduzidos por um decorador resultam em erros de tipo no TypeScript, exigindo soluções alternativas como mesclagem de interface, adicionando manualmente a anotação para os métodos adicionados ou simplesmente não usando decoradores para alterar classes (como se alguém precisasse ser informado de que poderíamos simplesmente não use decoradores).

Você não mencionou a opção de decorar a classe que está escrevendo quando ela é agnóstica para os membros apresentados pelo decorador, e fazer com que ela estenda uma classe decorada quando não for. É assim que eu estarei usando decoradores, pelo menos.

Você poderia me dar um trecho de código de uma biblioteca que você está usando agora, onde isso é um ponto problemático?

Desculpe - eu não segui seu último comentário, mas seu comentário antes disso, com a interface mesclada, é exatamente como estou trabalhando nisso.

Atualmente meu decorador precisa exportar também um Type, e eu uso a interface merging exatamente como você mostrou, para indicar ao TS que novos membros estão sendo adicionados pelo decorador. É a capacidade de abandonar esse clichê adicionado que muitos estão tão ansiosos.

@arackaf Talvez o que você realmente queira seja uma maneira de mesclar declarações para interagir com a digitação contextual de argumentos de função.

Certo. Eu quero a capacidade de usar um decorador em uma classe e, conceitualmente, com base em como eu defini o decorador, que alguma interface de mesclagem aconteça, resultando na minha classe decorada com membros extras adicionados.

Eu não tenho ideia de como eu faria para anotar meu decorador para que isso acontecesse, mas não sou muito exigente - qualquer sintaxe que uma versão futura do TS me deu para afetar isso me deixaria incrivelmente feliz :)

Os decoradores agora estão no estágio 2 e são usados ​​em praticamente todos os frameworks JS.

E eles estão apenas no estágio 2 e estão se movendo muito lentamente. Eu quero muito que os decoradores aconteçam, e estou tentando descobrir como posso ajudá-los a seguir em frente, mas eles ainda não são uma certeza. Ainda conheço pessoas que podem ter influência no processo e que se opõem a elas, ou as querem limitadas a anotações, embora não _acha_ que vão interferir. Meu ponto sobre as implementações atuais do decorador do compilador não sendo JS permanece.

Quanto ao @connect , o react-redux está com cerca de 2,5 milhões de downloads por mês; é incrivelmente arrogante ouvir que o design está de alguma forma "errado" porque você o teria implementado de forma diferente.

Por favor, evite ataques pessoais como me chamar de arrogante.

  1. Eu não te ataquei pessoalmente, então não me ataque pessoalmente.
  2. Não ajuda em nada o seu argumento.
  3. A popularidade de um projeto não deve excluí-lo da crítica técnica.
  4. Estamos falando sobre a mesma coisa? redux-connect-decorator tem 4 downloads por dia. A função connect() do react-redux se parece com um HOC, como eu sugeri.
  5. Eu acho que minha opinião de que decoradores bem comportados não devem mudar a forma pública de uma classe, e certamente devem substituí-la por uma classe não relacionada, é bastante razoável e encontraria um acordo suficiente em torno da comunidade JS. Está muito longe de ser arrogante, mesmo que você discorde.

As tentativas atuais de usar métodos introduzidos por um decorador resultam em erros de tipo no TypeScript, exigindo soluções alternativas como mesclagem de interface, adicionando manualmente a anotação para os métodos adicionados ou simplesmente não usando decoradores para alterar classes (como se alguém precisasse ser informado de que poderíamos simplesmente não use decoradores).

Não é que estamos apenas sugerindo não usar decoradores, é que acredito que @masaeedu está dizendo que, para fins de modificar um protótipo de maneira segura de tipo, temos mecanismos existentes: herança e aplicação de mixin. Eu estava tentando perguntar por que seu exemplo d não era adequado para ser usado como um mixin, e você não respondeu, exceto dizer que queria apenas usar decoradores.

Isso é bom, eu acho, mas parece limitar a conversa de forma bastante crítica, então vou me retirar novamente.

@justinfagnani eu estava falando

import {connect} from 'react-redux'

connect existe apenas uma função que recebe uma classe (componente) e cospe uma diferente, como você disse acima.

Atualmente, tanto no Babel quanto no TypeScript, tenho a opção de usar connect assim

class UnConnectedComponent extends Component {
}

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

Ou assim

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

Acontece que prefiro o último, mas se você ou qualquer outra pessoa preferir o primeiro, que assim seja; muitos caminhos levam a Roma. eu também prefiro

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

sobre

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

ou

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

Não respondi explicitamente à sua pergunta apenas porque achei que o motivo pelo qual estava usando decoradores era claro, mas vou declará-lo explicitamente: prefiro a ergonomia e a sintaxe que os decoradores fornecem. Este tópico inteiro é sobre tentar obter alguma capacidade de dizer ao TS como nossos decoradores estão mudando a classe em questão para que não precisemos de clichê como mesclagem de interface.

Como o OP disse há muito tempo, nós só 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'

para apenas trabalhar. O fato de existirem outras alternativas para essa sintaxe específica é óbvio e irrelevante. O fato de muitos na comunidade JS/TS não gostarem dessa sintaxe em particular também é óbvio e irrelevante.

Se o TS pudesse nos dar alguma maneira, ainda que restrita, de fazer essa sintaxe funcionar, seria um grande benefício para a linguagem e a comunidade.

@arackaf Você esqueceu de mencionar como o uso de connect exige o acesso a membros adicionais em this . AFAICT, sua implementação de componente deve ser totalmente agnóstica ao que connect faz, ele usa apenas seus próprios props e state . Então, nesse sentido, a maneira como o connect é projetado concorda mais com o uso de decoradores de @justinfagnani do que com o seu.

Na verdade, não. Considerar

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

então mais tarde

<Foo />

isso deve ser válido - os propsShape estão vindo da loja Redux, mas o TypeScript não sabe disso, já que está saindo da definição original de Foo, então acabo recebendo erros por adereços ausentes e preciso convertê-lo como any .

Mas, para ser claro, o caso de uso preciso originalmente fornecido pelo OP e duplicado no meu segundo comentário acima aqui, é extremamente comum em nossa base de código e, aparentemente, em muitos outros também. Nós literalmente usamos coisas como

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

e ter que adicionar alguma mesclagem de interface para satisfazer o TypeScript. Seria maravilhoso ter uma maneira de preparar essa interface mesclando-se à definição do decorador de forma transparente.

Continuamos andando em círculos com isso. Concordamos que o tipo de Foo deve ser o tipo de retorno de connect . Estamos de pleno acordo quanto a isso.

A diferença é se os membros dentro Foo precisam fingir que this é algum tipo de amálgama recursiva do tipo de retorno do decorador e o "Foo original", seja lá o que isso significa. Você não demonstrou um caso de uso para isso.

A diferença é se os membros dentro de Foo precisam fingir que isso é algum tipo de amálgama recursiva do tipo de retorno do decorador e o "Foo original", o que quer que isso signifique. Você não demonstrou um caso de uso para isso.

Eu tenho. Veja meu comentário acima e, nesse caso, o código original do OP. Este é um caso de uso extremamente comum.

Por favor, mostre-me o que connect adiciona ao protótipo que ele espera que você acesse dentro do componente. Se a resposta for "nada", então por favor não mencione connect novamente, pois é totalmente irrelevante.

@masaeedu é justo, e eu concordo. Eu estava respondendo principalmente às afirmações de @justinfagnani sobre como os decoradores não deveriam modificar a classe original, connect foi mal projetado, etc etc etc.

Eu estava apenas demonstrando a sintaxe que eu e muitos outros preferimos, apesar do fato de que outras opções existem.

Para este caso, sim, eu não conheço bibliotecas npm populares que recebem uma classe e devolvem uma nova classe com novos métodos, que seriam usados ​​dentro da classe decorada. Isso é algo que eu e outros fazemos com frequência, em nosso próprio código, para implementar nossos próprios mixins, mas eu realmente não tenho um exemplo no npm.

Mas não está realmente em disputa que este seja um caso de uso comum, não é?

@arackaf Não, não porque você não está acessando nenhum membro em this que é apresentado por @connect . Na verdade, tenho certeza de que este trecho preciso:

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

compilará sem problemas no TypeScript hoje. O snippet que você colou não tem nada a ver com o recurso que você está solicitando.

@masaeedu - desculpe - percebi que estava errado e excluí esse comentário. Não rápido o suficiente para evitar que você perca seu tempo :(

@arackaf Não sei o quão comum é um padrão e, pessoalmente, não o usei nem usei nenhuma biblioteca que o use. Eu usei Angular 2 por um tempo, então não é como se eu nunca tivesse usado decoradores na minha vida. É por isso que eu estava pedindo exemplos específicos de uso de uma biblioteca onde a incapacidade de acessar membros introduzidos pelo decorador no decorado é um ponto problemático.

Em todos os casos em que os membros de uma classe esperam algo de this , deve haver algo no corpo da classe ou na cláusula extends da classe que satisfaça o requisito. É assim que sempre funcionou, mesmo com decoradores . Se você tiver um decorador que adiciona alguma funcionalidade da qual uma classe depende, a classe deve estender o que o decorador retornar, e não o contrário. É apenas bom senso.

Não sei por que isso é senso comum. Este é o código da nossa base de código atualmente. Temos um decorador que adiciona um método ao protótipo de uma classe. Erros de TypeScript eliminados. Na época eu apenas joguei essa declaração lá. Eu poderia ter usado a mesclagem de interfaces.

Mas seria bom não usar nada. Seria muito, muito bom poder dizer ao TypeScript que "esse decorador aqui adiciona esse método a qualquer classe à qual seja aplicado - permita que this dentro da classe também o acesse"

Muitos outros neste tópico parecem estar usando decoradores de forma semelhante, então espero que você considere que pode não ser senso comum que isso não funcione.

image

@arackaf Sim, então você deveria estar fazendo export class SearchVm extends (@mappable({...}) class extends SearchVmBase {}) . É SearchVm que depende da mapeabilidade, e não o contrário.

classe de exportação SearchVm estende (@mappable({...}) classe estende SearchVmBase {})

O objetivo deste tópico é EVITAR clichês como esse. Decoradores fornecem um DX muito, muito melhor do que ter que fazer coisas assim. Há uma razão pela qual escolhemos escrever código como eu, em vez disso.

Se você ler os comentários deste tópico, espero que esteja convencido de que muitas outras pessoas estão tentando usar a sintaxe mais simples do decorador e, portanto, reconsiderem o que "devemos" estar fazendo ou o que é "senso comum", etc.

O que você quer não tem nada a ver especificamente com decoradores. Seu problema é que o mecanismo do TypeScript para dizer "hey TypeScript, coisas estão acontecendo com essa estrutura sobre as quais você não sabe nada, deixe-me contar tudo sobre isso", é atualmente muito limitado. No momento, temos apenas mesclagem de interface, mas você deseja que as funções sejam capazes de aplicar mesclagem de interface a seus argumentos, o que não está especificamente relacionado a decoradores de forma alguma.

No momento, temos apenas mesclagem de interface, mas você deseja que as funções sejam capazes de aplicar mesclagem de interface a seus argumentos, o que não está especificamente relacionado a decoradores de forma alguma.

OK. É justo. Você quer que eu abra um problema separado, ou você quer renomear este, etc?

@arackaf O único "boilerplate" é o fato de eu ter class { } , que é a) 7 caracteres fixos, não importa quantos decoradores você use b) resultado do fato de que os decoradores não podem (ainda ) ser aplicado a expressões de retorno de classe arbitrárias ec) porque eu queria especificamente usar decoradores para seu benefício. Esta formulação alternativa:

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

não é mais detalhado do que o que você está fazendo originalmente.

OK. É justo. Você quer que eu abra um problema separado, ou você quer renomear este, etc?

Questão separada seria bom.

não é mais detalhado do que o que você está fazendo originalmente

Não é mais detalhado, mas eu realmente espero que você considere o fato de que muitos simplesmente não gostam dessa sintaxe. Para melhor ou pior, muitos preferem a oferta dos decoradores DX.

Questão separada seria bom.

Vou digitar um amanhã, e link para este para o contexto.

Obrigado por ajudar a trabalhar com isso :)

Se me permite, @masaeedu discordo desta afirmação:

O que você quer não tem nada a ver especificamente com decoradores. Seu problema é que o mecanismo do TypeScript para dizer "hey TypeScript, coisas estão acontecendo com essa estrutura sobre as quais você não sabe nada, deixe-me contar tudo sobre isso", é atualmente muito limitado. No momento, temos apenas mesclagem de interface, mas você deseja que as funções sejam capazes de aplicar mesclagem de interface a seus argumentos, o que não está especificamente relacionado a decoradores de forma alguma.

Certamente o TypeScript poderia olhar para o tipo de retorno do decorador de classe para descobrir qual tipo a classe mutante deveria ser. Não há necessidade de "aplicar a mesclagem de interface aos argumentos". Então, a meu ver, é inteiramente a ver com decoradores.

Além disso, o caso do TypeScript não saber que um componente conectado não requer mais props é um aspecto que me desligou usando Redux com React e TypeScript.

@codeandcats Eu tenho andado em círculos com isso desde que o tópico começou e realmente não posso mais fazer isso. Por favor, olhe para a discussão anterior com cuidado e tente realmente entender o que eu estava discordando de @arackaf .

Concordo que quando você faz <strong i="8">@bar</strong> class Foo { } , o tipo de Foo deve ser o tipo de retorno de bar . Não concordo que this dentro Foo deva ser afetado de alguma forma. Usar connect é totalmente irrelevante para o ponto de contenção aqui, porque connect não espera que o componente decorado esteja ciente e use membros que ele está adicionando ao protótipo.

Eu posso entender que você está frustrado @masaeedu , mas não assuma apenas porque eu não tenho expressado ativamente minha opinião sobre isso que você está tendo nas últimas 48 horas que eu não tenho acompanhado a discussão - então, por favor poupe-me da condescendência companheiro.

Pelo que entendi, você acha que dentro de uma classe decorada ela não deveria saber de nenhuma mutação feita nela mesma pelos decoradores, mas fora dela deveria. Eu não discordo disso. O fato de @arackaf pensar que dentro de uma classe deveria ver a versão modificada está além do ponto do meu comentário.

De qualquer forma, o TypeScript pode usar o tipo retornado por uma função de decorador como fonte de verdade para uma classe. E, portanto, é um problema do Decorator, não uma nova funcionalidade de mutação de argumento bizarra como você sugere, que honestamente soa como uma tentativa de espantalho.

@Zalastax que está usando a função de decorador como uma função, não como um decorador. Se você tem vários decoradores que precisa aplicar, precisa aninha-los, o que não é tecnicamente mais detalhado, mas não é tão sintaticamente agradável - você sabe o que quero dizer?

condescendência etc

Ruído.

O fato de @arackaf pensar que dentro de uma classe deveria ver a versão modificada está além do ponto do meu comentário.

É o ponto central do parágrafo ao qual você está respondendo, portanto, se estiver falando de outra coisa, é irrelevante. O que @arackaf quer é, grosso modo, mesclar a interface em argumentos de função. Isso é melhor abordado por um recurso independente de decoradores. O que você quer (que aparentemente é idêntico ao que eu quero) é ter o bug no TypeScript corrigido onde <strong i="12">@foo</strong> class Foo { } não resulta na mesma assinatura de tipo para Foo que const Foo = foo(class { }) . Apenas este último está especificamente relacionado aos decoradores.

Pelo que entendi, você acha que dentro de uma classe decorada ela não deveria saber de nenhuma mutação feita nela mesma pelos decoradores, mas fora dela deveria. não discordo disso

Se você decidiu falar sobre algo sobre o qual estamos de acordo, mas prefaciá-lo com "eu discordo desta afirmação", então você está apenas perdendo tempo.

Posso obter algumas batatas fritas com este sal?

Ah, entendo, quando você apadrinha seus colegas, tudo bem, mas se as pessoas o puxam para cima, isso é barulho.

Não vejo por que seu conceito de "mutação de argumento" é necessário para implementar o que @arackaf propôs.

Independentemente de concordarmos com @arackaf ou não, o TypeScript poderia simplesmente inferir o tipo real do resultado do decorador, na minha humilde opinião.

Desculpas se você se sentiu condescendente; a intenção era fazer com que você
na verdade, uma linha gigantesca pela qual qualquer um poderia ser perdoado
deslizando. Eu mantenho isso, já que ainda está bem claro que você não está ciente
dos detalhes e estão "discordando" comigo com base em um mal-entendido
minha posição.

Por exemplo, sua pergunta sobre por que a interface se funde na função
argumentos resolve o problema de Adam é melhor respondido olhando para os
discussão com Adam, onde ele diz que já está usando a mesclagem de interface,
mas acha chato ter que fazer isso em cada site de declaração.

Acho que todos os aspectos técnicos disso foram espancados até a morte. eu
não quero que o tópico seja dominado por disputas interpessoais, então isso
vai ser meu último post.

É verdade, existem dois bugs com decoradores: o tipo de retorno não é respeitado, e dentro da classe, membros adicionados não são respeitados ( this dentro da classe). E sim, estou mais preocupado com o último, já que o primeiro é mais facilmente contornado.

Abri um caso separado aqui: https://github.com/Microsoft/TypeScript/issues/16599

Olá a todos, eu perdi essa conversa no fim de semana, e talvez não haja muita razão para trazê-la de volta agora; mas quero lembrar a todos que, no calor desse tipo de discussão, todos os que estão participando normalmente são bem-intencionados. Manter um tom respeitoso pode ajudar a esclarecer e orientar a conversa e pode estimular novos colaboradores. Fazer isso nem sempre é fácil (especialmente quando a internet deixa a tonalidade para o leitor), mas acho importante para cenários futuros. 😃

Qual é o status disso?

@alex94puchades ainda é uma proposta de estágio 2 , então provavelmente ainda temos um tempo. O TC39 parece ter algum movimento , pelo menos.

De acordo com este comentário , parece que poderia ser proposto para o estágio 3 já em novembro.

uma maneira alternativa de alterar a assinatura de uma função pelo decorador

adicionar uma função wapper vazia

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

adicionar definição

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

/ping

Se alguém estiver procurando uma solução para isso, uma solução alternativa que encontrei está abaixo.

Não é a melhor solução porque requer um objeto aninhado, mas para mim funciona bem porque eu realmente queria que as propriedades do decorador estivessem em um objeto e não apenas na instância da classe.

Isso é algo que estou usando para meus modais Angular 5.

Finja que tenho um decorador @ModalParams(...) que posso usar em ConfirmModalComponent . Para que as propriedades do decorador @ModalParams(...) apareçam no meu componente personalizado, preciso estender uma classe base que tenha uma propriedade à qual o decorador atribuirá seus valores.

Por exemplo:

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

Novamente, não é super bonito, mas funciona bem para o meu caso de uso, então pensei que alguém poderia achar útil.

@lansana mas você não entende os tipos não é?

@confraria Infelizmente não, mas pode haver uma maneira de conseguir isso se você implementar uma classe genérica Modal que você estende. Por exemplo, algo assim pode funcionar (não testado):

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

:/ sim, mas então o tipo é desacoplado do decorador e você certamente não pode usar dois deles ..:( além disso, você obteria erros se a classe não implementasse os métodos .. :( acho que não é uma maneira de acertar os tipos com este padrão no momento

Sim, seria ótimo se isso fosse possível, torna o TypeScript muito melhor e expressivo. Espero que algo aconteça em breve.

@lansana Sim, o ponto é que seria bom que os decoradores de classe fossem capazes de alterar a assinatura da classe por conta própria, sem exigir que a classe estendesse ou implementasse mais nada (já que isso é uma duplicação de esforço e informações de tipo) .

Nota lateral: no seu exemplo, lembre-se de que params seria estático em todas as instâncias de classes de componentes modais decoradas, pois é uma referência de objeto. Embora talvez seja por design. : - ) Mas eu discordo...

Edit: Ao pensar nisso, posso ver os contras de permitir que os decoradores modifiquem as assinaturas de classe. Se a implementação da classe claramente apresenta certas anotações de tipo, mas um decorador é capaz de mergulhar e mudar tudo isso, isso seria um truque meio ruim para os desenvolvedores. Muitos dos casos de uso evidentemente dizem respeito à incorporação de nova lógica de classe, então, infelizmente, muitos deles seriam melhor facilitados por meio de extensão ou implementação de interface - que também coordena com a assinatura de classe existente e gera erros apropriados onde ocorrem colisões. Um framework como o Angular, é claro, faz uso abundante de decoradores para aumentar as classes, mas o design não é permitir que a classe "ganhe" ou misture nova lógica do decorador que ele pode usar em sua própria implementação - é isolar lógica de classe da lógica de coordenação do framework. Essa é a minha _humilde_ opinião, de qualquer maneira. : - )

Parece melhor usar apenas classes de ordem superior em vez de decoradores + hacks. Eu sei que as pessoas querem usar decoradores para essas coisas, mas usar compose + HOCs é o caminho a seguir e provavelmente permanecerá assim... para sempre ;) De acordo com MS etc. decoradores são para anexar metadados apenas a classes e quando você verifique grandes usuários de decoradores como o Angular, você verá que eles são usados ​​apenas nessa capacidade. Duvido que você consiga convencer os mantenedores do TypeScript do contrário.

É triste e um pouco estranho que um recurso tão poderoso, que permitiria uma verdadeira composição de recursos, e que gera tal engajamento, tenha sido ignorado pela equipe TS por tanto tempo.

Este é realmente um recurso que pode revolucionar a forma como escrevemos código; permitindo que todos liberem pequenos mixins em coisas como Bitly ou NPM e tenham uma reutilização de código realmente incrível no Typescript. Em meus próprios projetos, eu faria instantaneamente meu @Poolable @Initable @Translating e provavelmente uma pilha a mais.

Por favor, toda a poderosa equipe principal do TS. "Tudo o que você precisa" para implementar é que as interfaces retornadas sejam honradas.

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

o que permitiria que este código fosse executado sem reclamação:

<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

Embora eu concorde com seus pontos, você pode conseguir a mesma coisa assim:

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

E então você nem precisaria de uma função init, você poderia fazer isso:

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

@lansana
Sim, mas isso poluiria meu construtor que nem sempre é super duper o que eu quero.

Também tenho um mixin semelhante para tornar qualquer objeto Poolable e outras coisas. Apenas a possibilidade de adicionar funcionalidade através da composição é o que é necessário. O exemplo init é apenas uma maneira necessária de fazer o que você fez sem poluir o construtor, tornando-o útil com outros frameworks.

@AllNamesRTaken Não faz sentido tocar nos decoradores agora, pois está nesse estado há tanto tempo e a proposta já está no estágio 2. Aguarde https://github.com/tc39/proposal-decorators para finalizar e, em seguida, provavelmente os receberemos da forma que todos concordarem.

@Kukkimonsuta
Discordo totalmente de você, pois o que estou solicitando é puramente sobre o tipo e não sobre a funcionalidade. Portanto, não tem muito a ver com coisas de ES. Minha solução acima já funciona, só não quero ter que transmitir para.

@AllNamesRTaken você pode fazer isso _hoje_ com mixins sem nenhum aviso:

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

No futuro, se colocarmos a proposta de mixins em JS, podemos fazer isso:

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 foi como eu comecei com esses recursos e minha implementação também funciona como você descreve:

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

o que é legal, como uma solução alternativa, mas não remove os méritos de permitir que os decoradores alterem o tipo na minha opinião.

EDIT: também perde a digitação na parcial para init.

Com este recurso você poderá escrever HoC mais facilmente
Espero que este recurso seja adicionado

@kgtkr essa é a principal razão pela qual estou querendo tanto isso ...

Há também uma pequena emergência nas definições react-router porque algumas pessoas decidiram que a segurança de tipo é mais importante do que ter uma interface de decoração de classe. A razão inferior é que withRouter torna alguns dos adereços opcionais.

Agora parece haver rivalidade, onde as pessoas são forçadas a usar a interface de função de withRouter em vez de decoração .

Começar a resolver esse recurso tornaria o mundo um lugar mais feliz.

O mesmo feud* ocorreu um tempo atrás com as tipagens material-ui e withStyle , que foi projetado para ser usado como decorador, mas que, para usar de maneira segura, os usuários do TypeScript precisam usar como uma função normal. Mesmo que a poeira tenha assentado principalmente sobre isso, é uma fonte de confusão contínua para os recém-chegados ao projeto!

* Bem, "rixa" pode ser uma palavra forte

Eu tenho observado isso por um longo tempo e, até que chegue, outros podem se beneficiar do meu pequeno truque para alcançar esse comportamento de maneira compatível com o futuro. Então aqui está, implementando um método super básico de classe de caso de estilo Scala copy ...

Eu tenho o decorador implementado assim:

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 cria uma classe anônima com o método copy anexado. Funciona exatamente como esperado em JavaScript quando em um ambiente com suporte a decoradores.

Para usar isso no TypeScript e realmente fazer com que o sistema de tipos reflita o novo método na classe de destino, o seguinte hack pode ser usado:

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

Todas as instâncias de MyCaseClass irão expor as tipagens para o método copy herdado da classe anônima dentro do decorador CaseClass . E, quando o TypeScript suporta a mutação de tipos declarados, esse código pode ser modificado facilmente para a sintaxe usual do decorador <strong i="18">@CaseClass</strong> etc sem nenhum sinal inesperado.

Seria ótimo ver isso na próxima versão principal do TypeScript - acredito que ajudará um código mais limpo e conciso, em vez de exportar uma bagunça estranha de classes de proxy.

Quando esse recurso estará disponível?

Eu queria usar um decorador de classe para cuidar de tarefas repetidas.
Mas agora ao inicializar a classe, recebo o seguinte erro: 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 esse recurso esteja disponível em breve! :)

@iainreid820 você experimentou algum bug estranho usando essa abordagem?

A longa espera! Enquanto isso, isso está sendo resolvido por alguma coisa no roteiro atual?
Como o problema 5453 ?

Estou usando o Material UI e só tive que perceber que o TypeScript não suporta a sintaxe do decorador para withStyles : https://material-ui.com/guides/typescript/#decorating -components

Corrija essa limitação na próxima versão do TypeScript. Decoradores de classe parecem bastante inúteis para mim agora.

Como mantenedor do Morphism Js , essa é uma grande limitação para esse tipo de biblioteca. Eu adoraria evitar que o consumidor de um decorador de função tivesse que especificar o tipo de destino da função https://github.com/nobrainr/morphism# --toclassobject-decorator, caso contrário, usar decoradores em vez de HOF parece um pouco inútil 😕
Existe algum plano para resolver isso? Existe uma maneira de ajudar a fazer esse recurso acontecer? Agradeço antecipadamente!

@bikeshedder que o exemplo de interface do usuário de material é um caso de uso de mixin de classe e você pode obter o tipo certo de mixins. Em vez de:

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

escrever:

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

@justinfagnani Isso não funciona para mim:

ss 2018-10-16 at 10 00 47

Aqui está meu código: https://gist.github.com/G-Rath/654dff328dbc3ae90d16caa27a4d7262

@G-Rath Acho que new () => React.Component<Props, State> deveria funcionar?

@emyann Sem dados. Eu atualizei a essência com o novo código, mas é isso que você quis dizer?

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

Não importa como você o formate, extends withStyles(styles)(...) não soa como uma sugestão adequada, fundamentalmente. withStyles NÃO é um mixin de classe.

withStyles pega a classe de componente A e cria a classe B que quando renderizada renderiza a classe A e passa props para ela + a prop classes .

Se você estender o valor de retorno de withStyles, estará estendendo o wrapper B , em vez de implementar a classe A que realmente recebe a propriedade classes .

Não importa como você o formate, extend withStyles(styles)(...) não soa como uma sugestão adequada, fundamentalmente. withStyles NÃO é um mixin de classe.

De fato. Não sei por que há tanta resistência aqui.

Dito isto, os decoradores estão extremamente próximos do estágio 3, pelo que ouvi, então imagino que o TS receberá o suporte adequado aqui em breve.

Eu ouço as pessoas que querem uma sintaxe mais limpa, esp. ao usar a aplicação de vários HoCs como decoradores. A solução temporária que usamos é realmente envolver vários decoradores dentro de uma função de pipe e, em seguida, usar essa função pura para decorar o componente. por exemplo

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

onde flow é lodash.flow . Muitas bibliotecas utilitárias fornecem algo assim - recompose , Rx.pipe etc. mas é claro que você pode escrever sua própria função pipe simples se não quiser instalar uma biblioteca.

Acho isso mais fácil de ler do que não usar decoradores,

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

Outra razão pela qual eu uso isso, é que esse padrão é facilmente encontrado e substituído/grep-able assim que a especificação do decorador é anunciada e suportada adequadamente.

@justinfagnani Estranhamente, recebo um erro de sintaxe do ESLint quando tento alterar extends React.Component<Props, State> para extends withStyles(styles)(React.Component<Props, State>) , então não consegui verificar o erro de tipo do @G-Rath da mesma maneira.

Mas notei que, se eu fizer algo como segue, um problema com new (talvez o mesmo problema?) aparece:

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

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

e o erro é:

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

Isso significa que o tipo retornado pelo Material UI não é um construtor (embora deva ser)?

@sagar-sm Mas, você obtém o tipo correto aumentado pelo tipo de interface do usuário do material? Não parece que você vai.

Para referência, tropecei nisso porque também estava tentando usar withStyles do Material UI como decorador, o que não funcionou, então fiz esta pergunta: https://stackoverflow.com/questions/53138167

precisar disso, então podemos fazer a função assíncrona retornar como bluebird

Tentei fazer algo parecido. ATM estou usando a seguinte solução alternativa:

Primeiro aqui está o meu 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>
        }
    }
}

Crie um decorador a partir da interface

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

E é assim que eu 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)

O truque é também criar uma interface com o mesmo nome da classe para criar uma declaração mesclada.
Ainda assim, a classe mostra apenas como tipo Test mas as verificações do texto datilografado estão funcionando.
Se você usar o decorador sem a notação @ e simplesmente invocá-lo como uma função, você obterá o tipo de interseção correto, mas perderá a capacidade de verificação de tipo dentro da própria classe, pois não pode usar o truque de interface mais e parece mais feio. Por exemplo:

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)

O que você diz sobre essas abordagens? Não é bonito, mas funciona como uma solução alternativa.

Há algum progresso nesta questão? Este parece ser um recurso muito, muito poderoso que desbloquearia muitas possibilidades diferentes. Suponho que esse problema esteja obsoleto agora porque os decoradores ainda são experimentais?

Seria muito bom ter uma declaração oficial sobre isso @andy-ms @ahejlsberg @sandersn. 🙏

Provavelmente não faremos nada aqui até que os decoradores sejam finalizados.

@DanielRosenwasser - o que significa "finalizado" aqui? O Estágio 3 no TC39 se qualifica?

Eu não sabia que eles tinham chegado tão longe! Quando chegaram ao estágio 3? Isso é recente?

Eles ainda não; Acredito que eles estão prontos para avançar do Estágio 2 para o Estágio 3 novamente na reunião do TC39 de janeiro.

Você pode ficar de olho na agenda para mais detalhes.

Correto (embora eu não possa confirmar que é para avançar em janeiro). Eu só estava perguntando se o estágio 3 se qualificaria quando eles chegassem lá.

Na minha opinião, a proposta do decorador ainda tem muitos problemas sérios, portanto, se avançar para o estágio 3 em janeiro, cometerá o erro novamente, assim como a proposta de campos de classe problemáticos e a proposta globalThis.

@hax você poderia elaborar?
Eu realmente gostaria disso e acho triste a falta de comunicação sobre o assunto e, infelizmente, não ouvi sobre os problemas.

@AllNamesRTaken Verifique a lista de problemas da proposta do decorador. 😆 Por exemplo, o argumento do decorador export antes/depois é um bloqueador do estágio 3.

Há também algumas mudanças propostas, como a API de redesenho, que acredito significar que a proposta não é estável para o estágio 3.

E também se relacionava com a proposta de campos de aula problemáticos. Embora os campos de classe tenham atingido o estágio 3, há muitos problemas. Um grande problema que me preocupa é deixar muitos problemas para os decoradores (por exemplo, protected ) e isso é ruim para ambas as propostas.

Observe que não sou delegados do TC39, e alguns delegados do TC39 nunca concordam com meus comentários sobre o status atual de muitos problemas. (Especialmente eu tenho forte opinião de que o atual processo TC39 tem grandes falhas em muitas questões controversas. Adiar algumas questões para os decoradores nunca resolvem o problema, apenas tornam a proposta do decorador mais frágil.)

Acho que essas coisas serão resolvidas e a proposta ficará bem, mas prefiro que não discutamos o estado da proposta aqui.

Acho que um estágio 3 com confiança suficiente ou estágio 4 é provavelmente onde implementaríamos a nova proposta e, então, poderíamos analisar essa questão.

Obrigado Danilo! O estágio 3 geralmente implica uma forte confiança no 4, então dedos cruzados para a reunião de janeiro.

E obrigado por evitar a discussão dos decoradores aqui. É bizarro o nível de raiva e queda de bicicleta que esse recurso causou. Nunca vi nada igual 😂

apenas para o registro, houve uma solicitação de recurso sobre a mesma coisa: https://github.com/Microsoft/TypeScript/issues/8545

irritantemente o TypeScript suporta esse recurso até certo ponto quando você compila do JavaScript (https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#better-handling-for-namespace-patterns-in -js-arquivos):

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

e até mesmo para o próprio código TypeScript, mas apenas quando se trata de funções (!)(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 Alguma palavra sobre a proposta de mudança para a Fase 3? Estou procurando construir uma biblioteca que adiciona funções de protótipo a uma classe ao usar um decorador.

Eles não avançaram na última reunião do TC39; eles podem ser promovidos novamente na próxima reunião. No entanto, não está claro; há muito no ar sobre a proposta neste momento.

As pessoas aqui que estão interessadas seriam aconselhadas a rastrear a proposta em si – o que manterá o ruído baixo para aqueles de nós assistindo ao tópico, bem como para os mantenedores do TS.

alguma atualização sobre este?

solução 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
    }
}

O que você acha dessa solução fácil "enquanto isso"?
https://stackoverflow.com/a/55520697/1053872

Mobx-state-tree usa uma abordagem semelhante com sua função cast() . Não parece bom, mas... Também não me incomoda muito. É fácil tirar sempre que algo melhor aparece.

Eu só gostaria de poder fazer algo nesse sentido ❤️
Não é esse o poder que os decoradores deveriam ter?

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

Passei algum tempo criando uma solução alternativa para digitar propriedades de classe decoradas. Eu não acho que será uma solução viável para os cenários deste tópico, mas você pode achar algumas partes úteis. Se você estiver interessado, você pode verificar meu post no Medium

Alguma atualização para esse problema?

Como estamos com isso?

Qual é o resultado deste problema, como funciona agora?

Você pode usar classes de mixin para obter esse efeito
https://mariusschulz.com/blog/mixin-classes-in-typescript

@Bnaya que não é totalmente o que queremos agora é ;).
Os decoradores nos permitem evitar a criação de classes que nunca pretendemos usar como se estivéssemos fazendo Java da velha escola. Os decoradores permitiriam uma arquitetura de composição muito limpa e organizada, onde a classe pode continuar a fazer a funcionalidade principal e ter uma funcionalidade extra generalizada composta nela. Sim, mixins podem fazer isso, mas é bandaid.

A proposta do decorador mudou significativamente nos últimos meses - é altamente improvável que a equipe TS invista algum tempo na implementação atual que será incompatível em breve™.

Eu recomendo assistir a seguintes recursos:

Proposta de decoradores: https://github.com/tc39/proposal-decorators
Roteiro TypeScript: https://github.com/microsoft/TypeScript/wiki/Roadmap

@Bnaya que não é totalmente o que queremos agora é ;).
Os decoradores nos permitem evitar a criação de classes que nunca pretendemos usar como se estivéssemos fazendo Java da velha escola. Os decoradores permitiriam uma arquitetura de composição muito limpa e organizada, onde a classe pode continuar a fazer a funcionalidade principal e ter uma funcionalidade extra generalizada composta nela. Sim, mixins podem fazer isso, mas é bandaid.

Não é mixagem, mas uma aula.
Não é a mistura de coisas ruins que
Quando o operador de pipeline chegar, a sintaxe também será razoável

Você pode usar classes de mixin para obter esse efeito

Mixins de fábrica de classe podem ser uma dor no TypeScript; mesmo que não fossem, eles são muito clichês, exigindo que você envolva classes em uma função para converter em um mixin, o que às vezes você não pode simplesmente fazer se estiver importando classes de terceiros.

O seguinte é muito melhor, mais simples, mais limpo e funciona com classes importadas de terceiros. Eu tenho isso funcionando bem em JavaScript simples sem transpilação além de transpilar o decorador de estilo legado, mas os class es permanecem 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()

E a saída no Chrome é:

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

Isso fornece completamente o que os mixins de fábrica de classe fazem, sem todo o clichê. Os wrappers de função em torno de suas classes não são mais necessários.

No entanto, como você sabe, o decorador não pode alterar o tipo da classe definida. 😢

Portanto, por enquanto, posso fazer o seguinte, com a única ressalva de que membros protegidos não são herdáveis:

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

O problema com a perda de membros protegidos é ilustrado com estes dois exemplos das implementações multiple (no que diz respeito aos tipos, mas a implementação de tempo de execução é omitida por questões de brevidade):

  • exemplo sem erro , porque todas as propriedades nas classes combinadas são públicas.
  • exemplo com erro (na parte inferior), porque os tipos mapeados ignoram os membros protegidos, neste caso tornando Two.prototype.two não herdável.

Eu realmente gostaria de uma maneira de mapear tipos, incluindo membros protegidos.

Isso seria muito útil para mim porque tenho um decorador que permite que uma classe seja chamada como uma função.

Isso funciona:

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

Isso não funciona:

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

Uma solução alternativa não tão difícil de limpar quando esse recurso sai pode ser feita usando a mesclagem de declaração e um arquivo de definição de tipos personalizados.

Já tendo isso:

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

Adicione o seguinte arquivo (arquivo a ser removido assim que o recurso for lançado):

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

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

Outra solução possível

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 em 2015 e ainda pendente. Existem muitos outros problemas relacionados e até artigos como este https://medium.com/p/caf24aabcb59/responses/show , tentando mostrar soluções alternativas (que são um pouco hacky, mas úteis)

É possível alguém lançar alguma luz sobre isso. Já está sendo considerado para discussão interna?

tl; dr - esse recurso está bloqueado no TC39. Não posso culpar o pessoal do TS por isso. Eles não vão implementar até que seja padrão.

Mas esta é a Microsoft, eles podem simplesmente implementá-lo e depois dizer ao pessoal dos padrões - "veja, as pessoas já estão usando nossa versão" :trollface:

A questão aqui é que a proposta dos decoradores mudou _significativamente_ (de maneiras incompatíveis com versões anteriores) desde que o TypeScript implementou os decoradores. Isso levará a uma situação dolorosa quando os novos decoradores forem finalizados e implementados, já que algumas pessoas estão confiando no comportamento e semântica do decorador atual do TypeScript. Suspeito fortemente que a equipe do TypeScript deseja evitar a ação desse problema porque isso incentivaria o uso adicional dos atuais decoradores não padrão, o que acabaria tornando a transição para qualquer nova implementação de decorador ainda mais dolorosa. Essencialmente, eu pessoalmente não vejo isso acontecendo.

Aliás, criei recentemente o #36348, que é uma proposta para um recurso que forneceria uma solução alternativa bastante sólida. Sinta-se à vontade para dar uma olhada / dar feedback sobre / selvagem. 🙂

tl; dr - esse recurso está bloqueado no TC39. Não posso culpar o pessoal do TS por isso. Eles não vão implementar até que seja padrão.

Dado esse fato, pode ser sensato para a equipe de desenvolvimento escrever uma explicação rápida e atualização de status e bloquear essa conversa até que o TC39 avance para o estágio necessário?

Alguém lendo isso tem algum peso no assunto?

Na verdade, praticamente todas as conversas relacionadas aos decoradores estão suspensas no ar. Exemplo: https://github.com/Microsoft/TypeScript/issues/2607

Obter alguma clareza sobre o futuro dos decoradores será útil, mesmo que se trate de pedir aos colaboradores que implementem a funcionalidade necessária. Sem orientação clara, o futuro dos decoradores é embaçado

Eu adoraria se a equipe do TypeScript pudesse assumir um papel proativo para a proposta do decorador, semelhante a como eles ajudaram a obter o encadeamento opcional. Eu também adoraria ajudar, mas ainda não trabalhei com desenvolvimento de linguagem e, portanto, não tenho certeza da melhor forma de fazê-lo :-/

Não posso falar por toda a equipe e pelo TypeScript como um projeto - mas acho improvável que façamos qualquer novo trabalho de decorador até que seja resolvido e confirmado, dado que a implementação do decorador já divergiu uma vez da implementação do TypeScript.

A proposta ainda está em desenvolvimento ativo e surgiu no TC39 esta semana https://github.com/tc39/proposal-decorators/issues/305

@orta , neste caso, acho que os documentos sobre o decorador de classes devem ser atualizados. Diz que

Se o decorador de classe retornar um valor, ele substituirá a declaração de classe pela função construtora fornecida.

Além disso, o exemplo a seguir dos documentos parece implicar que o tipo de instância Greeter teria a propriedade newProperty , o que não é verdade:

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

Acho que vale a pena acrescentar na documentação que a interface da classe retornada não substituirá a original.

Interessante, testei várias versões antigas do TypeScript e não consegui encontrar uma ocasião em que esse exemplo de código funcionasse ( exemplo de 3.3.3 ) - então sim, concordo.

Eu fiz https://github.com/microsoft/TypeScript-Website/issues/443 - se alguém souber como fazer esse classDecorator ter um tipo de interseção explícito, por favor comente nesse problema

Acabei aqui porque o problema com a documentação listada acima me causou confusão. Embora fosse ótimo se a assinatura de tipo do valor retornado do decorador fosse reconhecida pelo TS Compiler, em vez dessa alteração maior, concordo que a documentação deve ser esclarecida.

Para constar, esta é uma versão simplificada do que eu estava tentando em um formato os documentos relacionados do PR da @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);
Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

weswigham picture weswigham  ·  3Comentários

fwanicka picture fwanicka  ·  3Comentários

manekinekko picture manekinekko  ·  3Comentários

wmaurer picture wmaurer  ·  3Comentários

seanzer picture seanzer  ·  3Comentários