Typescript: Пожелание: мутация декоратора класса

Созданный на 20 сент. 2015  ·  231Комментарии  ·  Источник: microsoft/TypeScript

Если мы сможем правильно настроить проверку типов, у нас будет идеальная поддержка миксинов без шаблонов:

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

Самый полезный комментарий

То же самое было бы полезно для методов:

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

Предполагается, что асинхронный декоратор изменит тип возвращаемого значения на Promise<any> .

Все 231 Комментарий

То же самое было бы полезно для методов:

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

Предполагается, что асинхронный декоратор изменит тип возвращаемого значения на Promise<any> .

@Gaelan , это именно то, что нам здесь нужно! Это сделало бы миксины просто естественными для работы.

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 за это. Хотя я знаю, что это сложно реализовать и, вероятно, труднее достичь соглашения о семантике мутации декоратора.

+1

Если основным преимуществом этого является введение дополнительных членов в сигнатуру типа, вы уже можете сделать это с помощью слияния интерфейсов:

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

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

new Foo().foo();

Если декоратор - это реальная функция, которую компилятор должен вызвать, чтобы императивно изменить класс, это не похоже на идиоматическую вещь на языке, безопасном для типов, ИМХО.

@masaeedu Знаете ли вы какой-нибудь обходной путь для добавления статического члена в украшенный класс?

@davojan Конечно. Ну вот:

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

Также было бы полезно иметь возможность ввести _multiple_ свойства в класс при оформлении метода (например, помощник, который генерирует связанный сеттер для геттера или что-то в этом роде)

Типы реакции-редукции для connect принимают компонент и возвращают измененный компонент, реквизиты которого не включают подключенные реквизиты, полученные через редукцию, но, похоже, TS не распознает их определение connect как декоратор из-за этой проблемы. У кого-нибудь есть обходной путь?

Я думаю, что определение типа ClassDecorator нуждается в изменении.

В настоящее время это declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void; . Может быть, его можно изменить на

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;

Очевидно, что это отстой, и я понятия не имею, будет ли работать такая вещь (я просто пытаюсь преобразовать приложение Babel в машинописный текст и нажимаю на это).

@joyt Не могли бы вы предоставить реконструкцию проблемы на игровой площадке? Я не использую react-redux, но, как я уже упоминал ранее, я думаю, что любые расширения, которые вы хотите для формы типа, могут быть объявлены с помощью слияния интерфейсов.

@masaeedu вот основная разбивка движущихся частей..

По сути, декоратор предоставляет набор реквизитов для компонента React, поэтому общий тип декоратора — это подмножество декорируемого компонента, а не надмножество.

Не уверен, что это полезно, но попытался собрать невыполнимый образец, чтобы показать вам типы в игре.

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

Если вам нужен полный рабочий пример, я предлагаю открыть https://github.com/jaysoo/todomvc-redux-react-typescript или другой образец проекта react/redux/typescript.

Согласно https://github.com/wycats/javascript-decorators#class -declaration, насколько я понимаю, предложенный declare type WrappingClassDecorator = <TFunction extends Function, TDecoratorFunction extends Function>(target: TFunction) => TDecoratorFunction; недействителен.

Спецификация говорит:

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

переводится как:

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

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

Итак, если я правильно понимаю, то должно быть верно следующее:

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

Для вашего примера:

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

Я предполагаю, что вы имеете в виду, что F возвращает класс, который соответствует X (и не является экземпляром X )? Например:

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

В этом случае утверждения должны быть:

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

Foo , находящийся в области действия этих операторов let , был изменен декоратором. Исходный Foo больше недоступен. Это эффективно эквивалентно:

let Foo = F(class Foo {});

@nevir Да, ты прав. Спасибо за разъяснение.

С другой стороны, кажется, что отключить проверку на недействительность мутировавших типов классов относительно просто:

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

Но я недостаточно осведомлен, чтобы заставить компилятор выводить правильные определения типов мутировавшего класса. У меня есть следующий тест:

тесты/случаи/соответствие/декораторы/класс/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();

Он генерирует тесты/базовые показатели/местные/декораторонкласс10.типы

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

я ожидал
>C: typeof C будет >C: typeof X | typeof Y

Для тех, кто заинтересован в реакции connect в качестве примера использования этой функции, я зарегистрировал https://github.com/DefinitelyTyped/DefinitelyTyped/issues/9951 , чтобы отслеживать проблему в одном месте.

Я прочитал все комментарии по этой проблеме и понял, что подпись декоратора на самом деле не показывает, что он может делать с обернутым классом.

Рассмотрим это:

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

Это должно быть напечатано таким образом:
declare function decorator<T>(target: T): Wrapper<T>;

Но эта сигнатура не говорит нам, что декоратор добавил новые вещи в прототип цели.

С другой стороны, это не говорит нам, что декоратор действительно вернул обертку:
declare function decorator<T>(target: T): T & { someMethod: () => void };

Есть новости по этому поводу? Это было бы очень мощно для метапрограммирования!

Как насчет более простого подхода к этой проблеме? Для декорированного класса мы привязываем имя класса к возвращаемому декоратором значению в качестве синтаксического сахара.

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.

С точки зрения реализации это введет один синтетический символ для декорированного класса. И исходное имя класса привязано только к области тела класса.

@HerringtonDarkholme Я думаю, что это был бы очень прагматичный подход, который обеспечил бы большую часть желаемой выразительности. Отличная идея!

Я определенно хочу увидеть это когда-нибудь

Я часто пишу класс для Angular 2 или для Aurelia, который выглядит так:

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

Я хочу написать что-то вроде
декораторы/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;
    }
  }
}

и тогда я мог бы в целом применить его как

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

без потери безопасности типов. Это было бы благом для написания чистого, выразительного кода.

Возрождение этого вопроса.

Теперь, когда # 13743 отсутствует, и в языке есть поддержка миксинов, это очень полезная функция.

Однако @HerringtonDarkholme менее подходит для этого случая, поскольку необходимость объявлять возвращаемый тип декоратора теряет некоторые динамические функции...

@ahejlsberg , @mhegazy Как вы думаете, это выполнимо?

У меня есть другой сценарий использования, я не уверен, что он еще охвачен этим разговором, но, вероятно, подпадает под тот же зонтик.

Я хотел бы реализовать декоратор метода, который полностью меняет тип метода (не тип возвращаемого значения или параметры, а всю функцию). например

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

Вполне возможно в JavaScript, очевидно, это не проблема, но в TypeScript мне нужен декоратор asyncTask , чтобы изменить тип декорированного метода с () => Promise<void> на AsyncTask<() => Promise<void>> .

Уверен, что сейчас это невозможно и, вероятно, подпадает под действие этой проблемы?

@codeandcats ваш пример - это тот же вариант использования, для которого я здесь!

Привет @ohjames , прости меня, у меня проблемы с твоим примером, есть ли шанс, что ты сможешь переписать что-то, что работает так же, как на игровой площадке?

Есть ли прогресс в этом? Я держал это в голове весь день, не зная об этой проблеме, пошел реализовывать ее только для того, чтобы узнать, что компилятор ее не улавливает. У меня есть проект, который мог бы использовать лучшее решение для ведения журнала, поэтому я написал быстрый синглтон, чтобы позже расширить его до полноценного регистратора, который я собирался прикрепить к классам с помощью декоратора, такого как

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

и я написал для него необходимый код

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

и свойство logger определенно присутствует во время выполнения, но, к сожалению, не подхватывается компилятором.

Я хотел бы увидеть какое-то решение этой проблемы, тем более что такая конструкция времени выполнения должна быть абсолютно правильно представлена ​​во время компиляции.

В итоге я остановился на декораторе недвижимости, чтобы получить меня на данный момент:

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

и прикрепив его к классам, например

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

но это гораздо больше шаблонов для каждого класса, использующего регистратор, чем простой декоратор класса @loggable . Я полагаю, что мог бы привести к типу (this as Loggable<this>).logger , но это также довольно далеко от идеала, особенно после того, как я сделал это несколько раз. Это бы очень быстро надоело.

Мне пришлось использовать TS для всего приложения, в основном потому, что я не смог заставить https://github.com/jeffijoe/mobx-task работать с декораторами. Я надеюсь, что это будет рассмотрено в ближайшее время. 😄

Это очень раздражает в экосистеме Angular 2, где к декораторам и TypeScript относятся как к гражданам первого класса. Однако в ту минуту, когда вы пытаетесь добавить свойство с помощью декоратора, компилятор TypeScript говорит «нет». Я думал, что команда Angular 2 проявит некоторый интерес к этому вопросу.

@zajrik , вы можете добиться того, чего хотите, с помощью миксинов классов, которые поддерживаются с правильной типизацией, начиная с TS 2.2:

Определите свой миксин Loggable следующим образом:

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

и тогда вы можете использовать его несколькими способами. Либо в предложении extends объявления класса:

class Foo {
  superProperty: string;
}

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

TS знает, что экземпляры LoggableFoo имеют superProperty , logger и subProperty :

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

Вы также можете использовать миксин как выражение, которое возвращает конкретный класс, который вы хотите использовать:

const LoggableFoo = Loggable(Foo);

Вы _можете_ также использовать миксин класса в качестве декоратора, но он имеет немного другую семантику, в основном это подклассы вашего класса, а не разрешение вашему классу подклассировать его.

Примеси классов имеют несколько преимуществ перед декораторами, IMO:

  1. Они создают новый суперкласс, так что класс, к которому вы их применяете, имеет изменение, переопределяющее их.
  2. Они сейчас проверяют тип, без каких-либо дополнительных возможностей от TypeScript
  3. Они хорошо работают с выводом типов — вам не нужно вводить возвращаемое значение функции примеси.
  4. Они хорошо работают со статическим анализом, особенно с переходом к определению. Переход к реализации logger приводит вас к миксину _implementation_, а не к интерфейсу.

@justinfagnani Я даже не подумал о миксинах для этого, так что спасибо. Я пойду дальше и напишу миксин Loggable сегодня вечером, чтобы сделать мой синтаксис подключения Logger немного приятнее. Маршрут extends Mixin(SuperClass) я предпочитаю, так как я использовал миксины с момента выпуска TS 2.2.

Однако я предпочитаю идею синтаксиса декоратора миксинам, поэтому я все еще надеюсь, что можно найти какое-то решение для этой конкретной проблемы. На мой взгляд, возможность создавать примеси без шаблонов с помощью декораторов была бы огромным благом для более чистого кода.

@zajrik рад, что предложение помогло, надеюсь

Я до сих пор не совсем понимаю, почему у миксинов больше шаблонов, чем у декораторов. Они почти идентичны по синтаксическому весу:

Класс Миксин:

class LoggableFoo extends Loggable(Foo) {}

против декоратора:

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

На мой взгляд, примесь гораздо более ясна в своих намерениях: она генерирует суперкласс, а суперклассы определяют члены класса, так что примесь, вероятно, также определяет члены.

Декораторы будут использоваться для очень многих вещей, которые вы не можете предположить, являются или не являются определяющими членами. Это может быть просто регистрация класса для чего-то или связывание с ним некоторых метаданных.

Честно говоря, я думаю, что @zajrik хочет:

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

Что, несомненно, хоть и немного, но менее шаблонно.

Тем не менее, мне нравится раствор миксина. Я все время забываю, что миксины — это вещь.

Если все, что вам нужно, это добавить свойства к текущему классу, то примеси в основном эквивалентны декораторам с одной существенной неприятностью... если в вашем классе еще нет суперкласса, вам нужно создать пустой суперкласс, чтобы использовать их. Кроме того, синтаксис в целом кажется более тяжелым. Также неясно, поддерживаются ли параметрические миксины (разрешено ли extends Mixin(Class, { ... }) ).

@justinfagnani в вашем списке причин пункты 2-4 на самом деле являются недостатками TypeScript, а не преимуществами миксинов. Они не применяются в мире JS.

Я думаю, нам всем должно быть ясно, что решение проблемы OP на основе миксинов будет включать в себя добавление двух классов в цепочку прототипов, один из которых бесполезен. Это отражает семантические различия примесей и декораторов, однако примеси дают вам возможность перехватить цепочку родительских классов. Однако в 95% случаев это не то, что люди хотят делать, они хотят украсить этот класс. Хотя миксины имеют ограниченное применение, я думаю, что продвижение их в качестве альтернативы декораторам и классам более высокого порядка семантически неуместно.

Миксины в основном эквивалентны декораторам с одной существенной неприятностью... если в вашем классе еще нет суперкласса, вам нужно создать пустой суперкласс, чтобы использовать их.

Это не обязательно верно:

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

class Foo extends Mixin() {}

Кроме того, синтаксис в целом кажется более тяжелым.

Я просто не понимаю, как это так, поэтому нам придется не согласиться.

Также неясно, поддерживаются ли параметрические миксины (разрешены расширения Mixin(Class, { ... })).

Они очень. Миксины — это просто функции.

в вашем списке причин пункты 2-4 на самом деле являются недостатками TypeScript, а не преимуществами миксинов. Они не применяются в мире JS.

Это проблема TypeScript, поэтому они применимы здесь. В мире JS декораторов на самом деле еще не существует.

Я думаю, нам всем должно быть ясно, что решение проблемы OP на основе миксинов будет включать в себя добавление двух классов в цепочку прототипов, один из которых бесполезен.

Я не понимаю, где вы взяли два. Это один, как и декоратор, если только это не исправление. И какой прототип бесполезен? Приложение mixin предположительно добавляет свойство/метод, что не бесполезно.

Это отражает семантические различия примесей и декораторов, однако примеси дают вам возможность перехватить цепочку родительских классов. Однако в 95% случаев это не то, что люди хотят делать, они хотят украсить этот класс.

Я не уверен, что это правда. Обычно при определении класса вы ожидаете, что он будет находиться в нижней части иерархии наследования с возможностью переопределения методов суперкласса. Декораторы должны либо исправить класс, который имеет множество проблем, в том числе не работает с super() , либо расширить его, и в этом случае декорированный класс не имеет возможности переопределить расширение. Это может быть полезно в некоторых случаях, например, декораторы, которые переопределяют каждый определенный метод класса для отслеживания производительности/отладки, но это далеко от обычной модели наследования.

Хотя миксины имеют ограниченное применение, я думаю, что продвижение их в качестве альтернативы декораторам и классам более высокого порядка семантически неуместно.

Когда разработчик хочет добавить элементы в цепочку прототипов, миксины идеально подходят семантически. В каждом случае, когда я видел, что кто-то хочет использовать декораторы для примесей, использование примесей классов выполняло бы ту же задачу, с семантикой, которую они на самом деле ожидают от декораторов, большей гибкости благодаря рабочему свойству с супервызовами и Конечно, они работают сейчас.

Миксины вряд ли неуместны, когда они напрямую связаны с вариантом использования.

Когда разработчик хочет добавить участников в цепочку прототипов

Это именно то, что я хочу сказать, ОП не хочет ничего добавлять в цепочку прототипов. Он просто хочет мутировать один класс, и в основном, когда люди используют декораторы, у них даже нет родительского класса, кроме Object. И по какой-то причине Mixin(Object) не работает в TypeScript, поэтому вам нужно добавить фиктивный пустой класс. Итак, теперь у вас есть цепочка прототипов из 2 (не включая Object), когда она вам не нужна. Кроме того, добавление новых классов в цепочку прототипов сопряжено с нетривиальными затратами.

Что касается синтаксиса, сравните Mixin1(Mixin2(Mixin3(Object, { ... }), {... }), {...}) . Параметры для каждого миксина настолько далеки от класса миксинов, насколько это возможно. Синтаксис декоратора явно более удобочитаем.

Хотя синтаксис декоратора сам по себе не проверяет тип, вы можете просто использовать обычный вызов функции, чтобы получить то, что хотите:

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

Просто немного раздражает, что вам нужно объявить свой класс как const Foo = loggable(class { , но кроме этого все работает.

@ohjames (cc @justinfagnani) вы должны быть осторожны при расширении встроенных функций, таких как Object (поскольку они в экземплярах нарушают прототип вашего подкласса): https://github.com/Microsoft/TypeScript/wiki/FAQ # почему-не-расширение-встроенных-подобных-массивов-ошибок-и-карты-работает

@nevir да, я уже пытался использовать миксин с параметром по умолчанию Object в прошлом с TypeScript 2.2, и tsc отклоняет код.

@ohjames это все еще работает, вам просто нужно быть осторожным с прототипом в случае по умолчанию (см. эту запись часто задаваемых вопросов).

Хотя обычно проще полагаться на поведение tslib.__extend при передаче null

Есть ли планы сосредоточиться на этой проблеме на следующем этапе итерации? Преимущества этой функции чрезвычайно высоки для такого большого количества библиотек.

Я только что столкнулся с этой проблемой - она ​​заставляет меня писать много ненужного кода. Решение этой проблемы было бы огромным подспорьем для любого фреймворка/библиотеки на основе декоратора.

@TomMarius Как я упоминал ранее, классы, завернутые в функции декоратора, уже правильно проверяют тип, вы просто не можете использовать синтаксический сахар @ . Вместо того, чтобы делать:

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

вам просто нужно сделать:

const Foo = loggable(class { });

Вы даже можете составить кучу функций-декораторов вместе, прежде чем обернуть ими класс. Хотя правильная работа синтаксического сахара важна, не похоже, что это должно быть такой большой проблемой, как сейчас.

@masaeedu На самом деле проблема не во внешней, а во внутренней поддержке типов. Желаемым результатом, по крайней мере для меня, является возможность использовать свойства, которые декоратор добавляет внутри самого класса без ошибок компиляции. Приведенный вами пример даст только Foo регистрируемый тип, но не предоставит тип для самого определения класса.

@zajrik Декоратор возвращает новый класс из исходного класса, даже если вы используете встроенный синтаксис @ . Очевидно, что JS не обеспечивает чистоту, поэтому вы можете свободно изменять исходный класс, который вы передали, но это несовместимо с идиоматическим использованием концепции декоратора. Если вы тесно связываете функциональность, которую вы добавляете через декораторы с внутренними элементами класса, они также могут быть внутренними свойствами.

Можете ли вы привести пример использования класса, использующего внутренний API, который позже добавляется через декораторы?

Приведенный выше пример Logger является хорошим примером общего _want_ для возможности манипулировать внутренними компонентами декорированного класса. (И знаком людям, пришедшим из других языков с оформлением классов, таких как Python )

Тем не менее, предложение классного миксина @justinfagnani кажется хорошей альтернативой для этого случая.

Если вы хотите иметь возможность определять внутренности класса, структурированный способ сделать это не состоит в том, чтобы исправить класс или определить новый подкласс, и то, и другое TypeScript будет трудно рассуждать в контексте класса. себя, но либо просто определить вещи в самом классе, либо создать новый суперкласс, который имеет необходимые свойства, о которых может рассуждать TypeScript.

Декораторы действительно не должны изменять форму класса так, чтобы это было видно классу или большинству потребителей. @masaeedu прямо здесь.

Хотя то, что вы говорите, верно, TypeScript предназначен не для обеспечения соблюдения правил чистого кодирования, а для правильного ввода кода JavaScript, и в этом случае он терпит неудачу.

@masaeedu Что сказал @zajrik . У меня есть декоратор, который объявляет «онлайн-сервис», который добавляет в класс кучу свойств, которые затем используются в классе. Создание подклассов или реализация интерфейса невозможна из-за отсутствия метаданных и принудительного применения ограничений (если вы стремитесь избежать дублирования кода).

@TomMarius Я хочу сказать, что это правильная проверка типов. Когда вы применяете функцию декоратора к классу, класс никак не изменяется. Новый класс создается путем некоторого преобразования исходного класса, и только этот новый класс гарантированно поддерживает API, представленный функцией декоратора.

Я не знаю, что означает «отсутствующие метаданные и применение ограничений» (возможно, поможет конкретный пример), но если ваш класс явно полагается на API, введенный декоратором, он должен просто подклассировать его напрямую с помощью шаблона миксина , который показал @justinfagnani. или внедрить его через конструктор или что-то в этом роде. Полезность декораторов заключается в том, что они позволяют расширять закрытые для модификации классы в интересах кода, использующего эти классы . Если вы можете сами определить класс, просто используйте extends .

@masaeedu Если вы разрабатываете какую-то, скажем, RPC-библиотеку, и хотите заставить пользователя писать только асинхронные методы, подход, основанный на наследовании, заставляет вас дублировать код (или я не нашел правильного способа , может быть - буду рад, если подскажете, если знаете как).

Подход на основе наследования
Определение: export abstract class Service<T extends { [P in keyof T]: () => Promise<IResult>}> { protected someMethod(): Promise<void> { return Promise.reject(""); } }
Использование: export default class MyService extends Service<MyService> { async foo() { return this.someMethod(); } }

Подход на основе декоратора
Определение: export function service<T>(target: { new (): T & { [P in keyof T]: () => Promise<IResult> } }) { target.someMethod = function () { return Promise.reject(""); }; return target; }
Использование: <strong i="17">@service</strong> export default class { async foo() { return this.someMethod(); } }

Вы можете ясно увидеть дублирование кода в примере подхода, основанного на наследовании. Со мной и моими пользователями много раз случалось, что они забывали изменить параметр типа, когда копировали класс или начинали использовать «любой» в качестве параметра типа, и библиотека у них переставала работать; подход, основанный на декораторе, гораздо более удобен для разработчиков.

После этого есть еще одна проблема с подходом, основанным на наследовании: метаданные отражения теперь отсутствуют, поэтому вам приходится дублировать код еще больше, потому что вам все равно нужно вводить декоратор service . Использование теперь: <strong i="22">@service</strong> export default class MyService extends Service<MyService> { async foo() { return this.someMethod(); } } , и это просто недружелюбно, а не просто небольшое неудобство.

Вы правы в том, что семантически модификация выполняется после того, как класс определен, однако нет способа создать экземпляр необработанного класса, поэтому нет причин не поддерживать должным образом мутацию класса, кроме того, что она иногда допускает нечистый код (но иногда в лучшую сторону). Помните, что JavaScript по-прежнему основан на прототипах, а синтаксис класса — это просто сахар, чтобы прикрыть его. Прототип является изменяемым и может быть отключен декоратором, и он должен быть правильно типизирован.

Когда вы применяете функцию декоратора к классу, класс никак не изменяется.

Неправда, когда вы применяете функцию декоратора к классу, класс может быть изменен любым способом. Нравится тебе это или нет.

@TomMarius Вы пытаетесь использовать вывод для обеспечения соблюдения какого-то контракта, который совершенно не имеет отношения к обсуждаемому здесь аргументу. Вы должны просто сделать:

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

Нет абсолютно никаких требований, чтобы внутренние компоненты класса знали о функции декорирования.

@masaeedu Я не это имел в виду. Класс декоратора службы/службы вводит некоторые новые свойства, и они всегда доступны для использования классом, но система типов неправильно отражает это. Вы сказали, что я должен использовать для этого наследование, поэтому я показал вам, почему я не могу/не хочу этого делать.

Я отредактировал пример, чтобы сделать его более понятным.

@masaeedu Кстати, утверждение «Декоратор возвращает новый класс из исходного класса» неверно - каждый декоратор, который мы оба показали здесь, возвращает измененный или непосредственно исходный класс, а не новый.

@TomMarius В вашем комментарии упоминается проблема с «принуждением пользователя писать только асинхронные методы», которую я пытался решить в своем комментарии. Принуждение пользователя к соблюдению контракта, который вы ожидаете, должно выполняться всякий раз, когда код пользователя передается обратно в библиотеку, и не имеет ничего общего с обсуждением того, должны ли декораторы изменять форму типа, представленную внутренностям класса. Ортогональная проблема предоставления API пользовательскому коду может быть решена с помощью стандартных подходов наследования или композиции.

@ohjames Класс не изменяется простым применением декоратора. JS не требует чистоты, поэтому очевидно, что любой оператор в любом месте вашего кода может изменить что-либо еще, но это не имеет отношения к обсуждению этой функции. Даже после того, как эта функция будет реализована, TypeScript не поможет вам отслеживать произвольные структурные изменения в телах функций.

@masaeedu Ты собираешь кусочки, но я говорю об общей картине. Пожалуйста, просмотрите все мои комментарии в этой теме - дело не в отдельных проблемах, а в том, что каждая проблема происходит одновременно. Я думаю, что хорошо объяснил проблему с подходом, основанным на наследовании - много-много дублирования кода.

Для ясности, для меня это не про "чистый код". Проблема заключается в практичности; вам не нужны массовые изменения в системе типов, если вы относитесь к @foo так же, как к приложению функции. Если вы откроете банку червей, пытаясь ввести информацию о типе в аргумент функции из ее возвращаемого типа , в то же время взаимодействуя с выводом типа и всеми другими волшебными зверями, найденными в различных уголках системы типов TypeScript, я чувствую это станет большим препятствием для новых функций, почти так же, как сейчас перегрузка.

@TomMarius Ваш первый комментарий в этой теме касается чистого кода, который не имеет значения. Следующий комментарий относится к концепции онлайн-сервиса, для которого вы предоставили пример кода. Основная жалоба, начиная с первого абзаца и заканчивая четвертым, связана с тем, что использование MyService extends Service<MyService> подвержено ошибкам. Я попытался показать пример того, как вы можете справиться с этим.

Я посмотрел на него еще раз, и я действительно не вижу в этом примере ничего, что иллюстрировало бы, почему члены декорированного класса должны знать о декораторе. Что такого в этих новых свойствах, которые вы предоставляете пользователю, чего нельзя добиться с помощью стандартного наследования? Я не работал с отражением, поэтому я как бы пробежался по этому, мои извинения.

@masaeedu Я могу добиться этого с помощью наследования, но наследование вынуждает меня/моих пользователей массово дублировать код, поэтому я хотел бы иметь другой способ - и у меня есть, но система типов не может правильно отражать реальность.

Дело в том, что правильный тип <strong i="7">@service</strong> class X { } , где service объявлен как <T>(target: T) => T & IService , не X , а X & IService ; и проблема в том, что на самом деле это верно даже внутри класса, хотя семантически это неверно.

Еще одна большая проблема, которую вызывает эта проблема, заключается в том, что когда у вас есть ряд декораторов, каждый из которых имеет некоторые ограничения, система типов считает, что целью всегда является исходный класс, а не декорированный, и поэтому ограничение бесполезно.

Я могу сделать это с помощью наследования, но наследование вынуждает меня/моих пользователей массово дублировать код, поэтому я хотел бы иметь другой способ.

Это та часть, которую я не понимаю. Вашим пользователям необходимо реализовать IService , и вы хотите убедиться, что TheirService implements IService также подчиняется какому-то другому контракту { [P in keyof blablablah] } , и, возможно, вы также хотите, чтобы они имели { potato: Potato } на их службе. Все это легко выполнить, даже если члены класса не знают о @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 ""; }
}

Ни в том, ни в другом случае нет какого-то гигантского выигрыша в краткости, который может быть достигнут только путем представления возвращаемого типа serviceDecorator как типа от this до foo . Что еще более важно, как вы предлагаете придумать для этого хотя бы надежную стратегию набора текста? Тип возвращаемого значения serviceDecorator выводится на основе типа украшаемого вами класса, который, в свою очередь, теперь типизируется как возвращаемый тип декоратора...

Привет, @masaeedu , лаконичность становится особенно ценной, когда у тебя их несколько.

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

Однако этот обходной путь является лишь альтернативой декораторам классов. Для декораторов методов в настоящее время нет решений и блокирует столько хороших реализаций. Декораторы находятся в предложении на этапе 2 — https://github.com/tc39/proposal-decorators . Однако было много случаев, когда реализация была сделана намного раньше. Я думаю, что именно декораторы являются одним из тех важных кирпичиков, которые были действительно важны, поскольку они уже использовались во многих фреймворках, а очень простая версия уже реализована в babel/ts. Если бы эту проблему можно было реализовать, она не потеряла бы своего экспериментального состояния до официального релиза. Но это "экспериментально" для.

@pietschy Да, было бы неплохо заставить синтаксический сахар @ работать правильно с проверкой типов. На данный момент вы можете использовать композицию функций, чтобы получить достаточно подобную краткость:

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

const MyComponent = decorator(class {
});

Предыдущее обсуждение посвящено тому, является ли хорошей идеей сделать какую-то обратную типизацию, когда возвращаемый тип декоратора представляется членам класса как тип this .

Привет, @masaeedu , да, я понял контекст обсуждения, отсюда и // do stuff with undo context, component methods etc . Ваше здоровье

действительно нужно сделать Mixins проще.

typescript (javascript) не поддерживает множественное наследование, поэтому мы должны использовать Mixins или Traits.
А сейчас это отнимает у меня так много времени, особенно когда я что-то реконструирую.
И я должен везде копировать и вставлять интерфейс с его «пустой реализацией». (# 371)

--
Я не думаю, что возвращаемый тип декоратора должен быть представлен в классе.
потому что: .... эммм. не знаю, как это описать, извините за мой плохой английский ... ( 🤔 может ли быть фото без фоторамки ? ) эта работа стоит interface .

Добавлю к этому свой +1! Я хотел бы увидеть это в ближайшее время.

@pietschy Если класс зависит от членов, добавленных декораторами, то он должен расширять результат декораторов, а не наоборот. Ты должен сделать:

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

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

В качестве альтернативы система типов может работать в каком-то цикле, где аргумент функции-декоратора контекстуально типизируется по возвращаемому типу, который выводится из ее аргумента, который контекстуально типизируется по возвращаемому типу и т. д. Нам по-прежнему нужен конкретное предложение о том, как это должно работать, оно не просто ждет реализации.

Привет @masaeedu , я не понимаю, почему мне нужно создавать декораторы и применять их к базовому классу. Насколько я понимаю, прототип уже был изменен к моменту выполнения любого пользовательского кода земли. Например

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

Поэтому, если методы, определенные декораторами, уже существуют и могут быть законно вызваны, было бы неплохо, если бы машинописный текст отражал это.

Желание состоит в том, чтобы машинописный текст отражал это, не навязывая обходные пути. Если есть другие
вещи, которые могут делать декораторы, которые не могут быть смоделированы, было бы, по крайней мере, хорошо, если бы это
сценарий был поддержан в той или иной форме.

Эта ветка полна мнений о том, что должны делать декораторы, как их следует использовать, как их не следует использовать и т . д. до тошноты .

Вот что на самом деле делают декораторы:

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

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

что становится, если вы нацеливаетесь на 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);

с __decorate , применяя каждый декоратор снизу вверх, каждый из которых имеет возможность вернуть совершенно новый класс.

Я понимаю, что заставить систему типов поддерживать и признать это может быть очень сложно, и я понимаю, что для поддержки этого может потребоваться время, но не можем ли мы все согласиться с тем, что текущее поведение из исходного примера кода выше, который начал это нить, просто неправильно?

Независимо от того, что думает любой разработчик о декораторах, миксинах, функциональной композиции и т. д. и т. д., это кажется довольно явной ошибкой.


Могу ли я вежливо спросить, не планируется ли это в будущем выпуске?

TypeScript просто великолепен, и мне он нравится; однако это похоже на одну из немногих (единственных?) частей, которые просто сломаны, и я просто с нетерпением жду, когда это будет исправлено :)

@arackaf Хорошо известно, что на самом деле делают декораторы, и если вас не волнуют типы, которые испускает TypeScript, они уже поддерживают декораторы. Вся дискуссия в этом выпуске посвящена тому, как декорированные классы должны быть представлены в системе типов.

Я согласен с тем, что new Foo().foo , являющийся ошибкой типа, является ошибкой, и при этом ее легко исправить. Я не согласен с тем, что ошибка типа return this.foo; является ошибкой. Если что, вы просите фичу в системе типов, которую до сих пор никто в этом выпуске еще не указал. Если вы имеете в виду какой-то механизм, с помощью которого тип this должен быть преобразован декоратором, примененным к содержащему классу, вам нужно явно предложить этот механизм .

Такой механизм не так тривиален, как можно было бы ожидать, потому что почти во всех случаях возвращаемый тип декоратора выводится из того самого типа, который вы предлагаете преобразовать, используя возвращаемый тип декоратора. Если декоратор addMethod берет класс типа new () => T и производит new() => T & { hello(): void } , не имеет смысла предполагать, что T должен быть тогда T & { hello(): void } . Могу ли я ссылаться на super.hello в теле декоратора?

Это особенно уместно, потому что я не ограничен выполнением return class extends ClassIWasPassed { ... } в теле декоратора, я могу делать все, что захочу; типы вычитания, сопоставленные типы, союзы — все это честная игра. Любой результирующий тип должен хорошо работать с этим циклом вывода, который вы предлагаете. В качестве иллюстрации проблемы, пожалуйста, скажите мне, что должно произойти в этом примере:

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

Вы не можете просто отмахнуться от проблемы как от «сложной», кто-то должен действительно предложить, как это должно работать.

Это не ошибка — вы передаете Logger класс, соответствующий Greeter; однако затем декоратор мутирует Foo, чтобы он стал чем-то совершенно другим, что больше не расширяет Greeter. Таким образом, для декоратора регистратора должно быть допустимо рассматривать Foo как Greeter, но недопустимо для всех остальных, поскольку декоратор переназначает Foo чему-то совершенно другому.

Я уверен, что реализовать это будет очень сложно. Возможно ли какое-то разумное подмножество для распространенных случаев, таких как тот, что указан в верхней части этой темы, с обходными путями, такими как слияние интерфейсов, являющимися запасными вариантами для таких сложных пограничных случаев, как этот?

@arackaf

однако затем декоратор мутирует Foo, чтобы он стал чем-то совершенно другим

Нет точного определения того, что такое Foo . Помните, что весь наш спорный вопрос заключается в том, должны ли члены Foo иметь доступ (и, следовательно, возвращаться как часть общедоступного API), декоратор представил членов из this . Что такое Foo , определяется тем, что возвращает декоратор, а то, что возвращает декоратор, определяется тем, что такое Foo . Они неразрывно сращены.

Я уверен, что реализовать это будет ужасно сложно

Пока преждевременно говорить о сложности реализации, когда у нас даже нет внятного предложения о том, как эта фича должна работать. Извините, что повторяю это, но нам действительно нужен кто-то, кто предложит конкретный механизм того, "что происходит с this , когда я применяю такую ​​последовательность декораторов к такому классу". Затем мы можем подключить различные концепции TypeScript в разных частях и посмотреть, имеет ли смысл то, что получится. Только тогда имеет смысл обсуждать сложность реализации.

Текущий вывод декоратора, который я вставил выше, не соответствует спецификации? Я предположил, что это было из того, что я видел.

Предположим, что Foo имеет довольно точное значение на каждом этапе пути. Я ожидаю, что TS позволит мне использовать this (внутри методов Foo и экземпляров Foo) на основе того, что возвращает последний декоратор, при этом TS вынуждает меня добавлять аннотации типов по мере необходимости, если декоратор функции недостаточно поддаются анализу.

@arackaf Нет «шагов пути»; нас интересует только окончательный тип this для данного неизменяемого фрагмента кода. Вам необходимо описать, по крайней мере на высоком уровне, что, по вашему мнению, должен быть тип this для членов класса X , украшенных функциями декоратора f1...fn , в терминах сигнатур типа X и f1...fn . Вы можете иметь столько шагов в процессе, сколько захотите. До сих пор этим никто не занимался. Я догадывался, что люди имеют в виду, что возвращаемый тип декоратора должен быть представлен как тип this , но насколько я знаю, я мог быть совершенно не в тему.

Если ваше предложение состоит в том, чтобы механически заставить типы отражать то, что происходит со значениями в транспилированном выводе, вы получите то, что предлагаю я, а не то, что предлагаете вы: т. е. new Foo().hello() — это хорошо, но this.hello() нет. В этом примере исходный класс, который вы декорируете, нигде не получает метод hello . Только результат __decorate([addMethod], Foo) (который затем присваивается Foo ) имеет метод hello .

Я предполагаю, что люди имеют в виду, что возвращаемый тип декоратора должен быть представлен как тип этого

О, извините, да, верно. Именно это. Полная остановка. Потому что это то, что делают декораторы. Если последний декоратор в очереди возвращает какой-то совершенно новый класс, то это и есть Foo.

Другими словами:

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

Foo — это то, что говорит c в мире. Если c — это функция, которая возвращает any , тогда я не знаю — может быть, просто вернуться к исходному Foo ? Это кажется разумным подходом с обратной совместимостью.

но если c возвращает какой-то новый тип X , то я абсолютно ожидаю, что Foo будет уважать это.

Я что-то упускаю?


уточнение, если

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 

Я что-то упускаю?

@arackaf Да, чего не хватает этому наивному подходу, так это того, что декоратор может возвращать произвольные типы без ограничений на совместимость результата с типом Foo .

С этим можно придумать сколько угодно нелепостей. Скажем, я отклоняю свое возражение по поводу цикличности ввода this в результате c , который определяется типом this , который определяется результатом c и т. д. Это все еще не работает. Вот еще один бессмысленный пример:

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

В этом случае вы либо вынуждены выдавать ошибку для абсолютно безопасного кода, либо корректировать предложение, чтобы this было точно идентично возвращаемому типу декоратора.

Я не уверен, что вижу проблему. @logger решил вернуть совершенно новый класс без какого-либо метода foo и с совершенно новым методом hello() , который ссылается на исходный, теперь недостижимый Foo .

new Foo().foo()

больше не действует; это приведет к ошибке времени выполнения. Я просто говорю, что это также должно вызывать ошибку времени компиляции.

Тем не менее, если статический анализ всего этого слишком сложен, было бы совершенно разумно заставить нас добавить явные аннотации типа к logger , чтобы точно указать, что возвращается. И если такой аннотации типа нет, я бы сказал, просто вернитесь к предположению, что Foo возвращается обратно. Это должно поддерживать обратную совместимость.

@arackaf Нет проблем с кодом с точки зрения ввода или оценки во время выполнения. Я могу вызвать new Foo().hello() , который внутренне вызовет hello декорированного класса, который вызовет bar декорированного класса. Я не считаю ошибкой вызов bar внутри исходного класса.

Вы можете попробовать это сами, запустив этот полный пример на игровой площадке:

// Code from previous snippet...

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

Конечно, но я сказал, что вызывать

new Foo().foo()

Нет никаких проблем с кодом с точки зрения ввода или оценки во время выполнения. Я могу вызвать new Foo().hello(), который внутренне вызовет hello декорированного класса, который вызовет панель декорированного класса

Но это должно быть ошибкой сказать

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

поскольку метод приветствия Foo теперь возвращает void для класса, который возвращает Logger.

Конечно, но я сказал, что вызывать new Foo().foo() было ошибкой

@arackaf Но это не имеет значения. Я не вызывал new Foo().foo() . Я вызвал this.foo() и получил ошибку типа, хотя мой код отлично работает во время выполнения.

Но было бы ошибкой сказать let s : string = new Foo().hello()

Опять же, это не имеет значения. Я не говорю, что окончательный тип Foo.prototype.hello должен быть () => string (я согласен, что это должен быть () => void ). Я жалуюсь на ошибку действительного вызова this.bar() , потому что вы хирургическим путем пересадили тип, который бессмысленно пересаживать.

Здесь два Фу. Когда ты говоришь

class Foo { 
}

Foo — это неизменяемая привязка ВНУТРИ класса и изменяемая привязка вне класса. Так что это отлично работает, как вы можете проверить в jsbin

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

let F = Foo;

Foo = null;

new F().hello();

Ваш пример выше делает подобные вещи; он захватывает ссылку на исходный класс до того, как эта внешняя привязка будет изменена. Я все еще не уверен, к чему ты клонишь.

this.foo(); совершенно действителен, и я бы не ожидал ошибки типа (я также не стал бы винить людей TS, если бы мне нужна была какая-либо ссылка, поскольку я уверен, что отследить это будет сложно)

this.foo(); совершенно корректно, и я бы не ожидал ошибки типа

Хорошо, поэтому мы согласны, но это означает, что теперь вы должны уточнить или отклонить предложение о том, чтобы this был типом экземпляра того, что возвращает декоратор. Если вы считаете, что это не должно быть ошибкой типа, что должно быть this вместо { hello(): void } в моем примере?

this зависит от того, что было создано.

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

Можем ли мы остановиться на одном конкретном примере? Это значительно облегчило бы мне понимание. В следующем фрагменте:

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

Каков тип this ? Если это { hello(): void } , то я получу ошибку типа, потому что foo не является членом { hello(): void } . Если это не { hello(): void } , то this — это не просто тип экземпляра возвращаемого декоратором типа, и вам нужно объяснить любую альтернативную логику, которую вы используете, чтобы получить тип this .

РЕДАКТИРОВАТЬ: забыл добавить декоратор на Foo . Фиксированный.

this , где у вас есть стрелки, конечно, является экземпляром оригинального Foo . Ошибки типа нет.

Ах, теперь я понимаю вашу точку зрения; но я все еще не вижу, где проблема. this.foo() ВНУТРИ исходного Foo не является ошибкой типа — это допустимо для (теперь недостижимого) класса, который раньше был привязан к идентификатору Foo .

Это идиосинкразия, забавные мелочи, но я не понимаю, почему это должно мешать TS обрабатывать мутирующие декораторы классов.

@arackaf Ты не отвечаешь на вопрос. Каков конкретно тип this ? Вы не можете бесконечно циклически отвечать « this — это Foo, а Foo — это this ». Какие участники есть у this ? Если у него есть какие-либо элементы, кроме hello(): void , какая логика используется для их определения?

Когда вы говорите, что " this.foo() ВНУТРИ оригинального Foo не является ошибкой типа", вам все равно нужно ответить на вопрос: каков структурный тип this , чтобы это не было ошибкой типа для делать this.foo() ?

Кроме того, исходный класс не является «недоступным». Каждая функция, определенная в этом фрагменте кода, активно выполняется во время выполнения, и все работает без сбоев. Пожалуйста, запустите ссылку на игровую площадку, которую я предоставил, и посмотрите на консоль. Декоратор возвращает новый класс, в котором метод hello делегируется методу hello декорированного класса, который, в свою очередь, делегируется методу foo декорированного класса.

Это идиосинкразия, забавные мелочи, но я не понимаю, почему это должно мешать TS обрабатывать мутирующие декораторы классов.

В системе типов нет «пустяков». Вы не получите ошибку типа TSC-1234 «непослушный мальчик, вы не можете этого сделать», потому что вариант использования слишком нишевый. Если какая-то функция приводит к неожиданным поломкам совершенно нормального кода, эту функцию необходимо переосмыслить.

Вы не получите ошибку типа TSC-1234 «непослушный мальчик, вы не можете этого сделать», потому что вариант использования слишком нишевый.

Это ИМЕННО то, что я получаю, когда пытаюсь использовать метод, добавленный в определение класса декоратором. В настоящее время мне приходится обойти это, либо добавляя определение к классу, либо используя слияние интерфейсов, приведение к any и т. д.

Я ответил на все вопросы о том, что такое this и где.

Дело в том, что значение this меняется в зависимости от того, где вы находитесь.

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"

когда вы говорите new Class() - Class указывает на исходный Foo, и с точки зрения TypeScript он будет иметь доступ к hello(): string , поскольку это то, что класс набирается как (расширяет Greeter). Во время выполнения вы будете создавать экземпляр исходного Foo.

new LoggableFoo().hello() вызывает метод void, который вызывает метод, недоступный в противном случае, набранный через Greeter.

Если бы ты сделал

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

тогда Foo теперь является классом только с hello(): void и new Foo().foo() должно быть ошибкой типа.

И, опять же, hello() { return this.foo(); } не является ошибкой типа — с чего бы это? Тот факт, что это определение класса больше недоступно, не делает этот метод недействительным.

Я не ожидаю, что TypeScript идеально справится с любым из этих крайних случаев; было бы вполне понятно, если бы здесь и там приходилось добавлять аннотации типов, как всегда. Но ни один из этих примеров не показывает, почему @logger не может коренным образом изменить то, к чему привязана Foo .

Если logger — это функция, которая возвращает новый класс, то это то, на что теперь ссылается Foo.

Я ответил на все вопросы о том, что это такое и где.
Дело в том, что значение this меняется в зависимости от того, где вы находитесь.

Это очень расстраивает. Хорошо, он меняется, это Foo , это статическая привязка и т. д. и т. д. и т. д. Что такое сигнатура типа? Какие участники есть у this ? Вы говорите обо всем на свете, когда все, что мне нужно от вас, это простая подпись типа для того, что this находится внутри hello .

new LoggableFoo().hello() вызывает метод void, который вызывает метод, недоступный в противном случае, набранный через Greeter.

Это не то же самое, что недосягаемость. Каждый достижимый метод является «иначе недостижимым», если вы не учитываете пути, по которым он достижим.

Если бы вы сделали:

Но это буквально то, что я сделал. Это точно мой фрагмент кода, вставленный снова, с предисловием к объяснению примера, который я построил для вас. Я даже проверил его через средство проверки различий, чтобы убедиться, что не принимаю сумасшедшие таблетки, и единственное отличие — комментарий, который вы удалили.

И опять же, hello() { return this.foo(); } не является ошибкой типа — с чего бы это?

Потому что вы (и другие здесь) хотите, чтобы тип this был инстанцированным возвращаемым типом декоратора, то есть { hello(): void } (обратите внимание на отсутствие члена foo ). Если вы хотите, чтобы члены Foo могли видеть this в качестве возвращаемого типа декоратора, тип this внутри hello будет { hello(): void } . Если это { hello(): void } , я получаю ошибку типа. Если я получаю ошибку типа, мне грустно, потому что мой код работает нормально.

Если вы говорите, что это не ошибка типа, вы отказываетесь от собственной схемы предоставления типа this через возвращаемый тип декоратора. Тогда тип this равен { hello(): string; bar(): string } , независимо от того, что возвращает декоратор. У вас может быть какая-то альтернативная схема для создания типа this , которая позволяет избежать этой проблемы, но вам нужно указать, что это такое.

Вы, кажется, не понимаете, что после запуска декораторов Foo может ссылаться на что-то совершенно отличное от того, как оно было изначально определено.

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

Я предполагаю, что вы находите некоторую странность в том, что this является чем-то другим внутри Foo выше, чем в экземплярах, которые впоследствии создаются из того, чем в конечном итоге является Foo (после запуска декораторов)?

Я не совсем уверен, что вам сказать; именно так работают декораторы. Мой единственный аргумент заключается в том, что TypeScript должен более точно соответствовать тому, что происходит.

Иными словами, сигнатуры типов внутри (оригинального) Foo будут отличаться от того, что Foo представляет/производит после запуска декораторов.

Чтобы позаимствовать аналогию из другого языка, внутри украшенного Foo это будет ссылка на эквивалент анонимного типа C# — полностью реальный тип, который в остальном действителен, просто на него нельзя ссылаться напрямую. Было бы странно получать ошибки и не ошибки, описанные выше, но опять же, именно так это и работает. Декораторы дают нам огромную возможность делать такие причудливые вещи.

Я предполагаю, что вы находите некоторую странность в том, что это что-то другое внутри Foo выше, чем в экземплярах, которые впоследствии создаются из того, чем в конечном итоге является Foo (после запуска декораторов)?

Нет. Я не нахожу в этом ничего странного, потому что именно это я и предлагал 200 комментариев назад. Вы хоть что-то читали из предыдущего обсуждения?

Фрагмент, который вы опубликовали, совершенно бесспорен. Люди, с которыми я был не согласен и на чью помощь вы подскочили, также хотят следующее:

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

Я не согласен с тем, что это можно сделать правильно, и отчаянно пытался понять, как такое предложение будет работать. Несмотря на все мои усилия, я не могу получить исчерпывающий список членов, которые, как ожидается, будут иметь this , или то, как такой список будет составлен в соответствии с предложением.

Иными словами, сигнатуры типов внутри (оригинального) Foo будут отличаться от того, что Foo представляет/производит после запуска декораторов.

Так почему ты вообще споришь со мной? Я сказал: «Я согласен с тем, что new Foo().foo, являющийся ошибкой типа, является ошибкой, и при этом легко исправляемой. Я не согласен с тем, что return this.foo; ошибка типа является ошибкой». Аналогично, в вашем примере я согласен с тем, что ошибка типа new Foo().x() является ошибкой, а ошибка типа this.x() — нет.

Вы видите, что во фрагменте вверху этой страницы есть два комментария?

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

^ Это то, что я нахожу проблематичным. Либо вы соглашаетесь с тем, что возвращаемый тип декоратора не должен быть представлен на this , а только на new Foo() , и в этом случае мы оба спорим ни о чем. Или вы не согласны и тоже хотите эту функцию, и в этом случае фрагмент в вашем предыдущем комментарии не имеет значения.

Я наконец понял вашу мысль. Мне было очень трудно получить это из вашего кода Greeter, но сейчас я отслеживаю; спасибо за то, что вы так терпеливы.

Я бы сказал, что единственным разумным решением было бы для Foo ( внутри Foo) поддерживать объединение типов - derp, я имел в виду пересечение - исходного Foo и того, что возвращает последний декоратор. Вы должны поддерживать оригинальный Foo для сумасшедших примеров, таких как ваш Greeter, и вам определенно нужно поддерживать все, что возвращает последний декоратор, поскольку в этом весь смысл использования декораторов (согласно многим комментариям выше).

Так что да, из моего последнего примера внутри Foo x, y, z, a, b, c все будет работать. Если было две версии a , поддерживайте обе.

@arackaf np, спасибо за терпение. Мои примеры не были самыми ясными, потому что я просто публикую все, что могу приготовить на игровой площадке, демонстрируя, как это сломано. Мне трудно думать об этом систематически.

Я бы сказал, что единственным разумным решением было бы для Foo (внутри Foo) поддерживать объединение типов исходного Foo и все, что возвращает последний декоратор.

Хорошо, круто, так что мы входим в специфику этого сейчас. Поправьте меня, если я ошибаюсь, но когда вы говорите «объединение», вы имеете в виду, что он должен иметь элементы типа из обоих, то есть он должен быть A & B . Таким образом, мы хотим, чтобы this было равно typeof(new OriginalClass()) & typeof(new (decorators(OriginalClass))) , где decorators — сигнатура составного типа всех декораторов. Проще говоря, мы хотим, чтобы this было пересечением экземпляра типа «исходного класса» и экземпляра типа «исходного класса», прошедшего через все декораторы.

С этим есть две проблемы. Во-первых, в таких случаях, как мой пример, это просто позволяет вам получить доступ к несуществующим членам. Я мог бы добавить кучу членов в декоратор, но если бы я попытался получить к ним доступ в своем классе, используя this.newMethod() , меня просто вырвало бы во время выполнения. newMethod добавляется только к классу, возвращаемому функцией декоратора, члены исходного класса не имеют к нему доступа (если только я специально не использую шаблон return class extends OriginalClass { newMethod() { } } ).

Другая проблема заключается в том, что «исходный класс» не является четко определенным понятием. Если я могу получить доступ к оформленным членам из this , я также могу использовать их как часть операторов возврата, и, следовательно, они могут быть частью общедоступного API «исходного класса». Я как бы машу рукой, и я слишком выгорел, чтобы думать о конкретных примерах, но я думаю, что если бы мы хорошенько подумали об этом, мы могли бы придумать бессмысленные примеры. Возможно, вы могли бы обойти это, найдя какой-то способ разделения элементов, которые не возвращают то, к чему они обращались из this , или, по крайней мере, для которых тип возвращаемого значения не выводится в результате возврата this.something() .

@masaeedu да, я поправился насчет союза / пересечения до твоего ответа. Это нелогично для новичка в ТС.

По остальным понял. Честно говоря, декораторы обычно не возвращают совершенно другой тип, они обычно просто дополняют тип, который был передан, поэтому в большинстве случаев пересечение будет «просто работать» безопасно.

Я бы сказал, что ошибки времени выполнения, о которых вы говорите, будут редкими и будут результатом некоторых преднамеренно неправильных решений разработчиков. Я не уверен, что вам действительно нужно заботиться об этом, но, если это действительно проблема, я бы сказал, что простое использование того, что вернул последний декоратор, было бы приличным вторым местом (так что да, класс может увидеть TypeError на попытка использовать метод, который сам определяет - не идеальный, но все же достойная цена за работу декораторов).

Но на самом деле, я думаю, что ошибки времени выполнения, о которых вы думаете, не стоит предотвращать, особенно за счет корректной работы декораторов. Кроме того, если вы небрежны или глупы, в TS довольно легко допустить ошибки во время выполнения.

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

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

Относительно вашего второго возражения

Я также могу использовать их как часть операторов возврата, и, следовательно, они могут быть частью общедоступного API «исходного класса».

Боюсь, я не вижу в этом никаких проблем. Я хочу , чтобы все, что добавляется в класс через декоратор, было абсолютно первоклассным. Мне интересно, какие могут быть проблемы.

Я думаю, что еще одним хорошим вариантом было бы реализовать это только частично.

В настоящее время классы всегда типизируются по мере их определения. Таким образом, внутри Foo это основано на том, как определяется foo, независимо от декораторов. Было бы огромным, огромным улучшением «просто» расширить это для некоторого полезного подмножества вариантов использования декоратора (т.е. наиболее распространенных)

Что, если вы разрешите расширение класса (с точки зрения this внутри класса) тогда и только тогда, когда декоратор возвращает что-то, что расширяет оригинал, т.е.

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

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

Пусть он просто работает и обеспечивает первоклассную поддержку пустяков и всего остального, добавленного декоратором. А для случаев использования, которые делают сумасшедшие вещи, такие как возврат совершенно нового класса (например, ваш Greeter), просто продолжайте текущее поведение и игнорируйте то, что делает декоратор.


Кстати, независимо от того, что вы выберете, как бы я это прокомментировал? Можно ли это аннотировать в настоящее время? Я пытался

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

а также множество вариаций на эту тему, но у TypeScript ничего из этого не было :)

@arackaf Декоратор - это просто функция. В общем случае вы получите его где-то из файла .d.ts и понятия не имеете, как это реализовано. Вы не знаете, возвращает ли реализация совершенно новый класс, добавляя/вычитая/заменяя члены прототипа исходного класса и возвращая его, или расширяя исходный класс. Все, что у вас есть, — это структурный возвращаемый тип функции.

Если вы хотите каким-то образом связать декораторы с наследованием классов, вам нужно сначала предложить отдельную языковую концепцию для JS. То, как сегодня работают декораторы, не оправдывает мутацию this в общем случае. Например, я лично всегда предпочитаю композицию наследованию и всегда делаю:

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

Это не какой-то сумасшедший академический эксперимент, это стандартный подход к созданию миксинов, и он расходится с примером, который я вам привел. На самом деле почти все, кроме return class extends Class , не работает с примером, который я вам привел, и во многих случаях все будет безубыточно даже с return class extends Class .

Вам придется прыгать через все виды обручей и искажений, чтобы заставить это работать, и система типов будет бороться с вами на каждом этапе пути, потому что то, что вы делаете, в общем случае бессмысленно. Что еще более важно, как только вы ее реализуете, всем придется акробатически маневрировать вокруг этого бессмысленного уголка системы типов всякий раз, когда они пытаются реализовать какую-то другую сложную (но здравую) концепцию.

Просто потому, что у вас есть этот вариант использования, который вы считаете важным (и я несколько раз пытался в этой теме продемонстрировать альтернативные способы выразить то, что вы хотите, в существующей системе типов), не означает, что единственно правильный нужно любой ценой халтурить с предложенным вами подходом. Вы можете объединять произвольные интерфейсы с вашим классом, включая возвращаемые типы функций-декораторов, так что для вас не будет ничего невозможного, если вы настаиваете на использовании декораторов так, как вы их используете.

@arackaf это:

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

Должно работать нормально в предложении extends:

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

С гораздо более простой и уже определенной семантикой в ​​системе типов. Это уже работает. Какую проблему вы пытаетесь решить с помощью подклассов объявленного класса, а не создания для него суперкласса?

@masaeedu

То, как сегодня работают декораторы, не оправдывает изменения этого в общем случае.

Основное использование декораторов (класса) — абсолютно положительно изменить значение this внутри класса тем или иным образом. @connect @observer берут класс и выдают НОВУЮ версию исходного класса с дополнительными функциями. В этих конкретных случаях я не думаю , что фактическая структура this меняется (только структура this.props ), но это несущественно.

Это довольно распространенный (как вы можете видеть из комментариев выше) вариант использования декоратора для изменения класса). К лучшему или к худшему, людям просто не нравятся синтаксические альтернативы.

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

в отличие от

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

Теперь, делает ли декоратор

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

или

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

не должно иметь значения. Так или иначе, вариант использования, который многие действительно требуют, состоит в том, чтобы иметь возможность сообщить TypeScript с помощью любых необходимых аннотаций, независимо от того, насколько они неудобны для нас, что function c принимает класс C in и возвращает класс со структурой C & {blah(): void} .

Именно для этого многие из нас сегодня активно используют декораторы. Мы просто очень, очень хотим интегрировать полезное подмножество этого поведения в систему типов.

Легко продемонстрировать, что декораторы могут делать причудливые вещи, которые система типов не может отследить. Отлично! Но должен быть какой -то способ аннотировать это

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

является действительным. Я не знаю, потребуется ли новый синтаксис аннотации типа для c , или можно ли использовать существующий синтаксис аннотации типа для c для выполнения этого, но просто исправление общего использования случае (и оставить края как есть, без какого-либо воздействия на исходный класс) было бы огромным благом для пользователей TS.

@justinfagnani к вашему сведению

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

производит

Тип 'typeof (анонимный класс)' не может быть назначен типу 'T & (new () => {blah(): void; })'.
Тип «typeof (анонимный класс)» не может быть присвоен типу «T».

на детской площадке ТС. Не уверен, что некоторые параметры установлены неправильно.

Какую проблему вы пытаетесь решить с помощью подклассов объявленного класса, а не создания для него суперкласса?

Я просто пытаюсь использовать декораторы. Я с удовольствием использую их в JS уже больше года — я просто хочу, чтобы TS наверстал упущенное. И я, конечно, не женат на подклассах. Иногда декоратор просто видоизменяет исходный прототип класса, чтобы добавить свойства или методы. В любом случае, цель состоит в том, чтобы сообщить TypeScript, что

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

приведет к созданию класса с новыми членами, созданного c .

@arackaf Мы снова рассинхронизировались. Спор не об этом:

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

против этого:

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

Я ожидаю, что вы сможете использовать первое и по-прежнему будете правильно печатать new Foo() . Спор идет об этом:

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

против этого:

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

Последний будет работать без нарушения системы типов. Первое проблематично по нескольким причинам, которые я уже проиллюстрировал.

@masaeedu действительно ли нет ограниченных случаев использования, в которых первый можно было бы заставить работать?

Чтобы быть точным: нет ли аннотаций, которые TypeScript может сказать нам добавить к a , b и c из вашего примера выше, чтобы система типов обрабатывала первый неотличимо от последнего?

Это было бы убийственным достижением для TypeScript.

@arackaf извините, вам нужно сделать так, чтобы параметр типа ссылался на тип конструктора, а не на тип экземпляра:

Это работает:

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

Я просто пытаюсь использовать декораторы.

На самом деле это не та проблема, которую вы пытаетесь решить, это тавтология. Это как Q: "Что вы пытаетесь сделать с этим молотком?" A: «Просто используйте молоток».

Есть ли реальный случай, который вы имеете в виду, когда создание нового суперкласса с предполагаемым расширением не работает? Поскольку сейчас TypeScript поддерживает это, и, как я уже сказал, в этом случае гораздо проще рассуждать о типе класса изнутри класса.

Я посмотрел на аннотацию @observer в Mobx, и похоже, что она не меняет форму класса (и может легко быть декоратором метода, а не декоратором класса, поскольку он обертывает только render() ).

@connect на самом деле является отличным примером сложности правильного ввода декораторов, потому что @connect , кажется, даже не возвращает подкласс декорированного класса, а совершенно новый класс, который обертывает декорированный класс, но статика копируется, поэтому результат отбрасывает интерфейс на стороне экземпляра и сохраняет статическую сторону, которую TS в большинстве случаев даже не проверяет. Похоже, что @connect самом деле вообще не должен быть декоратором, а connect следует использовать просто как HOC, потому что декоратор ужасно сбивает с толку.

Я с удовольствием использую их в JS уже больше года.

Ну... на самом деле нет. В JS нет декораторов. Вы, вероятно, использовали конкретное несуществующее предыдущее предложение, которое реализовано немного по-разному в Babel и TypeScript. В других местах я выступаю за то, чтобы декораторы продолжали процесс стандартизации, но прогресс замедлился, и я не думаю, что они еще есть уверенность. Многие утверждают, что их не следует добавлять или что они должны быть только аннотациями, поэтому семантика может даже все еще оставаться в воздухе.

Одна из претензий к декораторам заключается именно в том, что они могут изменять форму класса, что затрудняет статические рассуждения, и обсуждались некоторые предложения, по крайней мере, ограничивающие возможности декораторов делать это, хотя я думаю, что это было до самое последнее предложение получило специальный язык. Я лично утверждаю, что корректные декораторы _не должны_ изменять форму класса, чтобы в целях статического анализа их можно было игнорировать. Помните, что в стандартном JS нет системы типов, на которую можно опереться, чтобы сказать анализу, что делает реализация функции.

  • Я просто хочу, чтобы ТС подтянулся.

Я не вижу, как здесь отстает TypeScript. Хотя бы за чем? Декораторы работают. Есть ли другой типизированный JS, в котором классы ведут себя так, как вы хотите?

Декораторы теперь относятся к этапу 2 и используются практически во всех JS-фреймворках.

Что касается @connect , react-redux загружается примерно 2,5 миллиона раз в месяц; невероятно высокомерно слышать, что дизайн какой-то «неправильный», потому что вы бы реализовали его по-другому.

Текущие попытки использовать методы, введенные декоратором, приводят к ошибкам типов в TypeScript, требующим обходных путей, таких как слияние интерфейсов, ручное добавление аннотаций для добавленных методов или просто отказ от использования декораторов для изменения классов (как если бы кому-то нужно было сказать, что мы можем просто не использовать декораторы).

Действительно ли нет никакого способа вручную, кропотливо сообщить компилятору TS, что декоратор меняет форму классов? Может быть, это сумасшествие, но не мог бы ТС просто отправить новый декоратортип, который мы могли бы использовать

пусть c : декоратор

И ЕСЛИ (и только если) декораторы этого типа применяются к классу, TS будет считать класс типом Input & {blah() }?

Буквально все знают, что можно просто не использовать декораторы. Большинство также знает, что вокальная группа не любит декораторов. Этот пост о том, как TS может каким-то образом заставить свою систему типов понять, что декоратор изменяет определение класса. Это было бы большим благом для многих людей.

Чтобы быть точным: нет ли каких-либо аннотаций, которые TypeScript мог бы сказать нам, чтобы добавить к a, b и c из вашего примера выше, чтобы система типов обрабатывала первое неотличимо от второго?

Да, очень легко объявить любую форму для Foo , которую вы хотите, вам просто нужно использовать слияние интерфейсов. Вам просто нужно сделать:

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

Прямо сейчас вам, вероятно, нужно ожидать, что любая библиотека декораторов, которую вы используете, экспортирует параметризованные типы, соответствующие тому, что они возвращают, или вам нужно будет объявить элементы, которые они добавляют, самостоятельно. Если мы получим #6606, вы можете просто сделать interface Foo extends typeof(a(b(c(Foo)))) .

Текущие попытки использовать методы, введенные декоратором, приводят к ошибкам типов в TypeScript, требующим обходных путей, таких как слияние интерфейсов, ручное добавление аннотаций для добавленных методов или просто отказ от использования декораторов для изменения классов (как если бы кому-то нужно было сказать, что мы можем просто не использовать декораторы).

Вы не упомянули возможность украсить класс, который вы пишете, когда он не зависит от членов, введенных декоратором, и сделать его расширением украшенного класса, когда это не так. По крайней мере, так я буду использовать декораторы.

Не могли бы вы дать мне фрагмент кода из библиотеки, которую вы используете прямо сейчас, где это проблема?

Извините - я не следил за вашим последним комментарием, но ваш предыдущий комментарий со слиянием интерфейса - это именно то, как я работаю над этим.

В настоящее время мой декоратор также должен экспортировать тип, и я использую слияние интерфейсов точно так, как вы показали, чтобы указать TS, что декоратор добавляет новые элементы. Это возможность избавиться от этого добавленного шаблона, которого так хотят многие.

@arackaf Возможно, вам действительно нужен способ объединения объявлений для взаимодействия с контекстной типизацией аргументов функции.

Конечно. Я хочу иметь возможность использовать декоратор в классе и концептуально, основываясь на том, как я определил декоратор, чтобы произошло некоторое слияние интерфейсов, в результате чего в моем декорированном классе были добавлены дополнительные члены.

Я понятия не имею, как я буду аннотировать мой декоратор, чтобы это произошло, но я не очень придирчив - любой синтаксис, который будущая версия TS дала мне, чтобы повлиять на это, сделал бы меня невероятно счастливым :)

Декораторы теперь относятся к этапу 2 и используются практически во всех JS-фреймворках.

И они только на стадии 2 и продвигаются крайне медленно. Я очень хочу, чтобы декораторы появились, и я пытаюсь понять, как я могу помочь им двигаться вперед, но они все еще не являются определенными. Я все еще знаю людей, которые могут иметь влияние на процесс, которые возражают против них или хотят, чтобы они ограничивались аннотациями, хотя я не думаю, что они будут вмешиваться. Моя точка зрения о том, что текущие реализации декораторов компилятора не являются JS, остается в силе.

Что касается @connect , react-redux загружается примерно 2,5 миллиона раз в месяц; невероятно высокомерно слышать, что дизайн какой-то «неправильный», потому что вы бы реализовали его по-другому.

Пожалуйста, воздержитесь от личных нападок, например, называя меня высокомерным.

  1. Я лично не нападал на вас, так что не нападайте лично на меня.
  2. Это никак не помогает вашему аргументу.
  3. Популярность проекта не должна исключать его из технической критики.
  4. Мы говорим об одном и том же? redux-connect-decorator скачивается 4 раза в день. Как я и предполагал, функция react-redux connect() выглядит как HOC.
  5. Я думаю, что мое мнение о том, что корректные декораторы не должны изменять публичную форму класса и, безусловно, должны заменить его несвязанным классом, довольно разумно и найдет достаточное согласие в сообществе JS. Это довольно далеко от высокомерия, даже если вы не согласны.

Текущие попытки использовать методы, введенные декоратором, приводят к ошибкам типов в TypeScript, требующим обходных путей, таких как слияние интерфейсов, ручное добавление аннотаций для добавленных методов или просто отказ от использования декораторов для изменения классов (как если бы кому-то нужно было сказать, что мы можем просто не использовать декораторы).

Дело не в том, что мы просто предлагаем вообще не использовать декораторы, я полагаю, что @masaeedu говорит, что для модификации прототипа типобезопасным способом у нас есть существующие механизмы: наследование и применение миксинов. Я пытался спросить, почему ваш пример d не подходит для использования в качестве примеси, и вы не дали ответа, кроме того, что сказали, что просто хотите использовать декораторы.

Думаю, это нормально, но кажется, что разговор довольно сильно ограничивается, так что я снова откланяюсь.

@justinfagnani я говорил о

import {connect} from 'react-redux'

connect есть просто функция, которая принимает класс (компонент) и выдает другой, как вы сказали выше.

В настоящее время как в Babel, так и в TypeScript у меня есть возможность использовать connect следующим образом.

class UnConnectedComponent extends Component {
}

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

ИЛИ вот так

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

Я предпочитаю последнее, но если вы или кто-либо другой предпочитаете первое, пусть будет так; многие дороги ведут в Рим. я так же предпочитаю

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

над

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

или

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

Я не ответил явно на ваш вопрос только потому, что думал, что причина, по которой я использую декораторы, ясна, но я заявлю это явно: я предпочитаю эргономику и синтаксис, которые предоставляют декораторы. Вся эта ветка посвящена попытке получить некоторую возможность сообщить TS, как наши декораторы мутируют рассматриваемый класс, поэтому нам не нужны стандартные шаблоны, такие как слияние интерфейсов.

Как сказал OP когда-то, мы просто хотим

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'

просто работать. Тот факт, что существуют другие альтернативы этому конкретному синтаксису, очевиден и несущественен. Тот факт, что многим в сообществе JS/TS не нравится именно этот синтаксис, также очевиден и несущественен.

Если бы TS мог дать нам какой-либо способ заставить этот синтаксис работать, пусть и ограниченный, это было бы огромным преимуществом для языка и сообщества.

@arackaf Вы не упомянули, что использование connect требует доступа к дополнительным участникам на this . AFAICT, реализация вашего компонента должна быть полностью независимой от того, что делает connect , она использует только свои собственные props и state . Таким образом, в этом смысле способ разработки connect больше согласуется с использованием декораторов @justinfagnani , чем с вашим.

Не совсем. Учитывать

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

тогда позже

<Foo />

это должно быть действительным - реквизиты PropsShape поступают из магазина Redux, но TypeScript не знает об этом, так как он выходит из исходного определения Foo, поэтому я получаю ошибки из-за отсутствующих реквизитов, и мне нужно преобразовать его как any .

Но чтобы было ясно, точный вариант использования, первоначально указанный OP и дублированный в моем втором комментарии выше, чрезвычайно распространен в нашей кодовой базе, и, судя по всему, также во многих других. Мы буквально используем такие вещи, как

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

и нужно добавить некоторое слияние интерфейсов, чтобы удовлетворить TypeScript. Было бы замечательно иметь способ запечь этот интерфейс, прозрачно сливаясь с определением декоратора.

Мы продолжаем ходить по кругу с этим. Мы согласны с тем, что тип Foo должен быть возвращаемым типом connect . Мы полностью согласны с этим.

Отличие заключается в том, должны ли члены внутри Foo притворяться, что this является своего рода рекурсивным объединением возвращаемого типа декоратора и «исходного Foo», что бы это ни значило. Вы не продемонстрировали вариант использования для этого.

Отличие заключается в том, должны ли члены внутри Foo делать вид, что это некое рекурсивное объединение возвращаемого декоратором типа и «исходного Foo», что бы это ни значило. Вы не продемонстрировали вариант использования для этого.

У меня есть. См. мой комментарий выше и, если на то пошло, исходный код OP. Это чрезвычайно распространенный вариант использования.

Пожалуйста, покажите мне, что connect добавляет к прототипу, к которому он ожидает, что вы получите доступ внутри компонента. Если ответ «ничего», то, пожалуйста, не поднимайте снова connect , так как это совершенно не имеет значения.

@masaeedu достаточно честно, и я согласен. В основном я отвечал на утверждения @justinfagnani о том, что декораторы не должны изменять исходный класс, connect был плохо спроектирован и т. д. и т. д. и т. д.

Я просто продемонстрировал синтаксис, который предпочитаю я и многие другие, несмотря на то, что существуют и другие варианты.

В этом случае, да, я не знаю популярных библиотек npm, которые берут класс и выдают новый класс с новыми методами, которые будут использоваться внутри декорированного класса. Это то, что я и другие часто делаем в нашем собственном коде, чтобы реализовать наши собственные миксины, но у меня нет примера на npm.

Но на самом деле не оспаривается, что это распространенный вариант использования, не так ли?

@arackaf Нет, это не так, потому что вы не получаете доступ ни к каким членам на this , которые представлены @connect . На самом деле я вполне уверен, что этот точный фрагмент:

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

сегодня без проблем скомпилируется в TypeScript. Фрагмент, который вы вставили, не имеет ничего общего с запрашиваемой функцией.

@masaeedu - извините - я понял, что ошибался, и удалил этот комментарий. Однако недостаточно быстро, чтобы не тратить время попусту :(

@arackaf Я не знаю, насколько это распространенный шаблон, и я лично не использовал его сам и не использовал какие-либо библиотеки, которые его используют. Я какое-то время использовал Angular 2, так что не то чтобы я никогда в жизни не пользовался декораторами. Вот почему я просил конкретные примеры использования библиотеки, где невозможность доступа к введенным декоратором членам в декорации является болевой точкой.

В каждом случае, когда члены класса ожидают что-то вроде this , в теле класса или в предложении класса extends должно быть что-то, что удовлетворяет требованию. Так это всегда работало, даже с декораторами . Если у вас есть декоратор, который добавляет некоторую функциональность, от которой зависит класс, класс должен расширять то, что возвращает декоратор, а не наоборот. Это просто здравый смысл.

Я не уверен, почему это здравый смысл. Это код из нашей кодовой базы на данный момент. У нас есть декоратор, который добавляет метод к прототипу класса. Ошибки TypeScript. В то время я просто кинул туда эту декларацию. Я мог бы использовать слияние интерфейсов.

Но было бы неплохо вообще ничего не использовать. Было бы очень, очень приятно иметь возможность сказать TypeScript, что «здесь этот декоратор добавляет этот метод к любому классу, к которому он применяется — разрешить this внутри класса также доступ к нему»

Похоже, что многие другие в этой ветке используют декораторы аналогичным образом, поэтому я надеюсь, что вы примете во внимание, что это может не сработать.

image

@arackaf Да, так что вы должны делать export class SearchVm extends (@mappable({...}) class extends SearchVmBase {}) . Именно SearchVm зависит от возможности сопоставления, а не наоборот.

класс экспорта SearchVm extends (@mappable({...}) class extends SearchVmBase {})

Весь смысл этой ветки в том, чтобы ИЗБЕГАТЬ подобных шаблонов. Декораторы обеспечивают гораздо более приятный DX, чем подобные вещи. Есть причина, по которой мы решили писать код, как я, а не так.

Если вы прочитаете комментарии к этой ветке, я надеюсь, вы убедитесь, что многие другие люди пытаются использовать более простой синтаксис декоратора и, следовательно, пересматривают то, что мы «должны» делать, или что такое «здравый смысл» и т. д.

То, что вы хотите, не имеет ничего общего с декораторами. Ваша проблема в том, что механизм TypeScript, позволяющий сказать: «Привет, TypeScript, с этой структурой происходит что-то, о чем вы ничего не знаете, позвольте мне рассказать вам все об этом», в настоящее время слишком ограничен. Прямо сейчас у нас есть только слияние интерфейсов, но вы хотите, чтобы функции могли применять слияние интерфейсов к своим аргументам, что никак не связано с декораторами.

Прямо сейчас у нас есть только слияние интерфейсов, но вы хотите, чтобы функции могли применять слияние интерфейсов к своим аргументам, что никак не связано с декораторами.

В порядке. Справедливо. Вы хотите, чтобы я открыл отдельную тему, или вы хотите переименовать эту и т.д.?

@arackaf Единственным «шаблоном» является тот факт, что у меня было class { } , то есть а) фиксированные 7 символов, независимо от того, сколько декораторов вы используете б) результат того факта, что декораторы не могут (пока ) можно применять к произвольным выражениям, возвращающим класс, и c) потому что я специально хотел использовать декораторы в ваших интересах. Эта альтернативная формулировка:

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

не более многословен, чем то, что вы делаете изначально.

В порядке. Справедливо. Вы хотите, чтобы я открыл отдельную тему, или вы хотите переименовать эту и т.д.?

Хорошо бы отдельную тему.

не более многословен, чем то, что вы делаете изначально

Это не более многословно, но я очень надеюсь, что вы учтете тот факт, что многим просто не нравится такой синтаксис. Хорошо это или плохо, но многие предпочитают предложение декораторов DX.

Хорошо бы отдельную тему.

Я напечатаю один завтра и дам ссылку на этот для контекста.

Спасибо, что помогли справиться с этим :)

Если позволите, @masaeedu , я не согласен с этим утверждением:

То, что вы хотите, не имеет ничего общего с декораторами. Ваша проблема в том, что механизм TypeScript, позволяющий сказать: «Привет, TypeScript, с этой структурой происходит что-то, о чем вы ничего не знаете, позвольте мне рассказать вам все об этом», в настоящее время слишком ограничен. Прямо сейчас у нас есть только слияние интерфейсов, но вы хотите, чтобы функции могли применять слияние интерфейсов к своим аргументам, что никак не связано с декораторами.

Конечно, TypeScript мог бы посмотреть на возвращаемый тип декоратора класса, чтобы понять, каким типом должен быть мутировавший класс. Нет необходимости «применять слияние интерфейсов к аргументам». Так что, как я вижу, это полностью связано с декораторами.

Кроме того, тот факт, что TypeScript не знает, что подключенному компоненту больше не нужны реквизиты, является одним из аспектов, который отключил меня от использования Redux с React и TypeScript.

@codeandcats Я ходил по кругу с этим с тех пор, как начался поток, и я действительно больше не могу этого делать. Пожалуйста, внимательно посмотрите на предыдущее обсуждение и постарайтесь понять, в чем я не согласен с @arackaf .

Я согласен, что когда вы делаете <strong i="8">@bar</strong> class Foo { } , тип Foo должен быть возвращаемым типом bar . Я не согласен с тем, что this внутри Foo должно быть каким-либо образом затронуто. Использование connect здесь совершенно не имеет отношения к предмету разногласий, потому что connect не ожидает, что декорированный компонент будет знать и использовать члены, которые он добавляет к прототипу.

Я понимаю, что вы расстроены @masaeedu , но не думайте, что только потому, что я не высказывал активно свое мнение в течение последних 48 часов, что я не следил за обсуждением - поэтому, пожалуйста Избавь меня от снисходительности, приятель.

Насколько я понимаю, вы думаете, что внутри декорированного класса он не должен знать о каких-либо мутациях, сделанных над ним декораторами, но должен знать снаружи. Я не согласен с этим. Тот факт, что @arackaf считает, что внутри класса должна быть видна мутированная версия, не является предметом моего комментария.

В любом случае TypeScript может использовать тип, возвращаемый функцией декоратора, в качестве источника истины для класса. И, следовательно, это проблема декоратора, а не какая-то новая причудливая функциональность мутации аргументов, как вы предлагаете, что, честно говоря, просто звучит как попытка соломенного человека.

@Zalastax , который использует функцию декоратора как функцию, а не как декоратор. Если у вас есть несколько декораторов, которые вам нужно применить, вам нужно их вложить друг в друга, что технически уже не многословно, но не так приятно синтаксически — понимаете, о чем я?

снисходительность и др.

Шум.

Тот факт, что @arackaf считает, что внутри класса должна быть видна мутированная версия, не является предметом моего комментария.

Это центральный пункт абзаца, на который вы отвечаете, поэтому, если вы говорите о чем-то другом, это не имеет значения. То, что хочет @arackaf , - это, грубо говоря, слияние интерфейсов с аргументами функции. Это лучше решается функцией, независимой от декораторов. То, что вы хотите (что, по-видимому, идентично тому, что я хочу), - это исправить ошибку в TypeScript, где <strong i="12">@foo</strong> class Foo { } не приводит к подписи того же типа для Foo , что и const Foo = foo(class { }) . Только последний конкретно относится к декораторам.

Насколько я понимаю, вы думаете, что внутри декорированного класса он не должен знать о каких-либо мутациях, сделанных над ним декораторами, но должен знать снаружи. Я не согласен с этим

Если вы решили поговорить о чем-то, в чем мы согласны, но предварить это словами «Я не согласен с этим утверждением», то вы просто теряете время.

Можно мне картошку фри с этой солью?

О, я вижу, когда вы покровительствуете своим сверстникам, это нормально, но если люди тянут вас за это, это шум.

Я не понимаю, почему ваша концепция «мутации аргументов» необходима для реализации того, что предложил @arackaf .

Независимо от того, согласны мы с @arackaf или нет, по моему скромному мнению, в любом случае TypeScript может просто вывести фактический тип из результата декоратора.

Извините, если вы чувствовали себя снисходительно; цель состояла в том, чтобы заставить вас
на самом деле гигантская бессвязная нить, которую можно простить любому
скимминг. Я поддерживаю это, так как все еще довольно ясно, что вы не в курсе
деталей и "не согласны" со мной на основании непонимания
моя позиция.

Например, ваш вопрос о том, почему слияние интерфейсов в функции
аргументы решают проблему Адама, лучше всего ответить, посмотрев соответствующие
дискуссия с Адамом, где он говорит, что уже использует слияние интерфейсов,
но находит раздражающим делать это на каждом сайте объявлений.

Я думаю, что все технические аспекты этого были избиты до смерти. я
не хочу, чтобы в треде преобладали межличностные ссоры, так что это
будет мой последний пост.

Это правда, с декораторами есть две ошибки: возвращаемый тип не соблюдается, и внутри класса добавленные члены не соблюдаются ( this внутри класса). И да, меня больше беспокоит последнее, так как первое легче обойти.

Я открыл здесь отдельный кейс: https://github.com/Microsoft/TypeScript/issues/16599 .

Привет всем, я пропустил этот разговор на выходных, и, возможно, сейчас нет причин поднимать его снова; но я хочу напомнить всем, что в пылу подобных дискуссий каждый, кто принимает участие, обычно действует из лучших побуждений. Сохранение уважительного тона может помочь прояснить и направить разговор, а также привлечь новых участников. Сделать это не всегда легко (особенно когда Интернет оставляет тональность на усмотрение читателя), но я думаю, что это важно для будущих сценариев. 😃

Каков статус по этому поводу?

@alex94puchades это все еще предложение второго этапа , так что у нас, вероятно, еще есть время. По крайней мере, у TC39 есть какое-то движение .

Согласно этому комментарию , похоже, что его могут предложить для этапа 3 уже в ноябре.

обходной путь для изменения подписи функции декоратором

добавить пустую функцию wapper

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

добавить определение

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

результат

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

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

/пинг

Если кто-то ищет решение этой проблемы, один обходной путь, который я придумал, приведен ниже.

Это не лучшее решение, потому что для него требуется вложенный объект, но для меня это прекрасно работает, потому что я на самом деле хотел, чтобы свойства декоратора находились в объекте, а не просто в экземпляре класса.

Это то, что я использую для своих модальных окон Angular 5.

Представьте, что у меня есть декоратор @ModalParams(...) , который я могу использовать для пользовательского ConfirmModalComponent . Чтобы свойства декоратора @ModalParams(...) отображались в моем пользовательском компоненте, мне нужно расширить базовый класс, у которого есть свойство, которому декоратор будет присваивать свои значения.

Например:

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

Опять же, это не очень красиво, но хорошо работает для моего варианта использования, поэтому я подумал, что кому-то еще это может пригодиться.

@lansana , но ты не понимаешь типов, не так ли?

@confraria К сожалению, нет, но может быть способ добиться этого, если вы реализуете общий класс Modal , который вы расширяете. Например, что-то вроде этого может работать (не проверено):

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

:/ да, но тогда тип отделяется от декоратора, и вы, конечно, не можете использовать два из них.. :( кроме того, вы получите ошибки, если класс не будет реализовывать методы.. :( я не думаю, что есть это способ получить правильные типы с помощью этого шаблона на данный момент

Да, было бы здорово, если бы это было возможно, делая TypeScript намного лучше и выразительнее. Надеюсь, скоро что-то из этого выйдет.

@lansana Да, дело в том, что было бы неплохо, если бы декораторы классов могли самостоятельно изменять сигнатуру класса, не требуя, чтобы класс расширял или реализовывал что-либо еще (поскольку это дублирование усилий и информации о типе) .

Примечание: в вашем примере помните, что params будет статическим для всех экземпляров классов декорированных модальных компонентов, поскольку это ссылка на объект. Хотя может так задумано. :-) Но я отвлекся...

Редактировать: когда я думаю об этом, я вижу минусы в том, что декораторы могут изменять сигнатуры классов. Если в реализации класса явно присутствуют определенные аннотации типов, но декоратор способен вмешаться и изменить все это, это будет немного подлый трюк для разработчиков. Очевидно, что многие варианты использования связаны с включением новой логики класса, поэтому, к сожалению, многие из них было бы лучше упростить с помощью реализации расширения или интерфейса, которая также согласуется с существующей сигнатурой класса и вызывает соответствующие ошибки при возникновении коллизий. Фреймворк, такой как Angular, конечно, обильно использует декораторы для расширения классов, но дизайн не позволяет классу «получать» или смешивать новую логику от декоратора, которую он затем может использовать в своей собственной реализации — это изолировать логика класса из логики координации фреймворка. Во всяком случае, это мое _скромное_ мнение. :-)

Кажется, лучше просто использовать классы более высокого порядка, а не декораторы + хаки. Я знаю, что люди хотят использовать декораторы для этого, но использование compose + HOC - это путь, и он, вероятно, останется таким ... навсегда;) Согласно MS и т. Д., Декораторы предназначены для прикрепления метаданных только к классам и когда вы проверьте крупных пользователей декораторов, таких как Angular, вы увидите, что они используются только в этом качестве. Сомневаюсь, что вы сможете убедить разработчиков TypeScript в обратном.

Грустно и немного странно, что такая мощная фича, которая позволяла бы настоящую композицию фич и вызывала такое вовлечение, так долго игнорировалась командой TS.

Это действительно функция, которая может произвести революцию в том, как мы пишем код; позволяя всем выпускать небольшие миксины для таких вещей, как Bitly или NPM, и иметь действительно потрясающее повторное использование кода в Typescript. В своих собственных проектах я бы мгновенно сделал свой @Poolable @Initable @Translating и, возможно, еще кучу.

Пожалуйста, вся могучая основная команда TS. «Все, что вам нужно» для реализации, это то, что возвращаемые интерфейсы должны соблюдаться.

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

что позволит этому коду работать без жалоб:

<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

Хотя я согласен с вашими пунктами, вы можете добиться того же самого:

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

И тогда вам даже не понадобится функция инициализации, вы можете просто сделать это:

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

@лансана
Да, но это загрязнило бы мой конструктор, который не всегда супер-пупер, что я хочу.

Также у меня есть аналогичный миксин, чтобы сделать любой объект доступным для пула и другие вещи. Просто возможность добавить функциональность через композицию — это то, что нужно. Пример с инициализацией — это просто необходимый способ сделать то, что вы сделали, не загрязняя конструктор, что делает его полезным с другими фреймворками.

@AllNamesRTaken Сейчас нет смысла трогать декораторов, так как они так долго находились в этом состоянии, а предложение уже находится на стадии 2. Дождитесь завершения https://github.com/tc39/proposal-decorators , и тогда мы, скорее всего, получим их в той форме, с которой все согласны.

@Куккимонсута
Я категорически с вами не согласен, поскольку то, что я прошу, касается исключительно типа, а не функциональности. Следовательно, это не имеет большого отношения к материалам ES. Мое решение выше уже работает, я просто не хочу приводить к.

@AllNamesRTaken , вы можете сделать это _сегодня_ с миксинами вообще без предупреждений:

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

В будущем, если мы получим предложение примесей в JS, мы сможем сделать это:

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 был тем, как я начал с этих функций, и моя реализация на самом деле также работает, как вы описываете:

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

что неплохо, как обходной путь, но на самом деле не умаляет достоинств разрешения декораторам изменять тип, на мой взгляд.

РЕДАКТИРОВАТЬ: также он теряет ввод на частичном для инициализации.

С помощью этой функции вам будет легче писать HoC.
надеюсь эту функцию добавят

@kgtkr это главная причина, почему я так сильно этого хочу...

В определениях react-router также есть небольшой экстренный случай, потому что некоторые люди решили, что безопасность типов важнее, чем интерфейс оформления классов. Основная причина в том, что withRouter делает некоторые реквизиты необязательными.

Теперь, кажется, вражда, когда люди вынуждены использовать функциональный интерфейс withRouter вместо украшения .

Если начать решать эту функцию, мир станет счастливее.

Та же вражда* произошла некоторое время назад с типизацией material-ui и withStyle , которая была разработана для использования в качестве декоратора, но для безопасного использования пользователям TypeScript необходимо использовать как обычная функция. Несмотря на то, что пыль в основном осела на этом, это источник постоянной путаницы для новичков в проекте!

* Ну, "вражда" может быть сильно сказано

Я наблюдал за этим в течение долгого времени, и пока он не появится, другие, надеюсь, смогут извлечь выгоду из моего небольшого хака, чтобы добиться такого поведения с прямой совместимостью. Итак, вот оно, реализация супер-базового метода case class copy в стиле Scala...

У меня декоратор реализован так:

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

Этот код создает анонимный класс с прикрепленным методом copy . Он работает точно так, как ожидается в JavaScript, в среде, поддерживающей декораторы.

Чтобы использовать это в TypeScript и фактически заставить систему типов отражать новый метод в целевом классе, можно использовать следующий хак:

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

Все экземпляры MyCaseClass будут предоставлять типы для унаследованного метода copy от анонимного класса внутри декоратора CaseClass . И, когда TypeScript поддерживает мутацию объявленных типов, этот код можно легко изменить до обычного синтаксиса декоратора <strong i="18">@CaseClass</strong> etc без каких-либо неожиданных ошибок.

Было бы здорово увидеть это в следующем крупном выпуске TypeScript — я считаю, что это поможет сделать код более чистым и лаконичным, вместо того, чтобы экспортировать странный беспорядок прокси-классов.

Когда эта функция станет доступна?

Я хотел использовать декоратор класса, чтобы позаботиться о повторяющейся задаче.
Но теперь при инициализации класса я получаю следующую ошибку: 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.

Я надеюсь, что эта функция будет доступна в ближайшее время! :)

@ iainreid820 сталкивались ли вы с какими-либо странными ошибками при использовании этого подхода?

Долгое ожидание! Между тем, решается ли это чем-нибудь в текущей дорожной карте?
Например, проблема 5453 ?

Я использую Material UI и просто должен был понять, что TypeScript не поддерживает синтаксис декоратора для withStyles : https://material-ui.com/guides/typescript/#decorating -components

Пожалуйста, исправьте это ограничение в следующем выпуске TypeScript. Сейчас декораторы классов кажутся мне довольно бесполезными.

Как сопровождающий Morphism Js , это большое ограничение для такого рода библиотек. Я бы хотел, чтобы потребителю декоратора функций не приходилось указывать целевой тип функции https://github.com/nobrainr/morphism# --toclassobject-decorator, иначе использование декораторов вместо HOF звучит немного бесполезно. 😕
Есть ли какой-нибудь план по решению этой проблемы? Есть ли способ помочь реализовать эту функцию? Заранее спасибо!

@bikeshedder , что пример Material UI является примером использования миксина класса, и вы можете получить правильный тип из миксинов. Вместо:

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

написать:

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

@justinfagnani У меня это не работает:

ss 2018-10-16 at 10 00 47

Вот мой код: https://gist.github.com/G-Rath/654dff328dbc3ae90d16caa27a4d7262

@ G-Rath Я думаю, new () => React.Component<Props, State> вместо этого должно работать?

@emyann Нет игральных костей. Я обновил суть новым кодом, но это то, что вы имели в виду?

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

Независимо от того, как вы его отформатируете, extends withStyles(styles)(...) в принципе не звучит как правильное предложение. withStyles НЕ является миксином класса.

withStyles берет класс компонента A и создает класс B , который при рендеринге отображает класс A и передает ему реквизиты + реквизит classes .

Если вы расширяете возвращаемое значение withStyles, то вы расширяете оболочку B вместо реализации класса A , который фактически получает реквизит classes .

Независимо от того, как вы его форматируете, extends withStyles(styles)(...) в принципе не звучит как правильное предложение. withStyles НЕ является миксином класса.

Верно. Не знаю, почему здесь так много отпора.

Тем не менее, декораторы очень близки к этапу 3, насколько я слышал, поэтому я думаю, что TS скоро получит здесь надлежащую поддержку.

Я слышу людей, которые хотят более чистый синтаксис, особенно. при использовании применения нескольких HoC в качестве декораторов. Временный обходной путь, который мы используем, заключается в том, чтобы фактически обернуть несколько декораторов внутри функции канала, а затем использовать эту чистую функцию для украшения компонента. например

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

где flow равно lodash.flow . Многие библиотеки утилит предоставляют что-то вроде этого - recompose , Rx.pipe и т. д., но вы, конечно, можете написать свою собственную простую функцию канала, если вы не хотите устанавливать библиотеку.

Я считаю, что это легче читать, чем не использовать декораторы,

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

Еще одна причина, по которой я использую это, заключается в том, что этот шаблон легко найти и заменить / использовать grep после того, как спецификация декоратора объявлена ​​​​и должным образом поддерживается.

@justinfagnani Как ни странно, я получаю синтаксическую ошибку от ESLint, когда пытаюсь изменить extends React.Component<Props, State> на extends withStyles(styles)(React.Component<Props, State>) , поэтому я не смог проверить ошибку типа @G-Rath таким же образом.

Но я заметил, что если я делаю что-то вроде следующего, появляется проблема с new (может быть, та же проблема?):

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

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

и ошибка:

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

Означает ли это, что тип, возвращаемый Material UI, не является конструктором (хотя должен им быть)?

@ sagar-sm Но вы получаете правильный тип, дополненный типом пользовательского интерфейса материала? Не похоже, что вы будете.

Для справки, я наткнулся на это, потому что я также пытался использовать Material UI withStyles в качестве декоратора, который не работал, поэтому я задал этот вопрос: https://stackoverflow.com/questions/53138167

нужно это, тогда мы можем заставить асинхронную функцию возвращаться как bluebird

Я пытался сделать что-то подобное. Банкомат Я использую следующий обходной путь:

Сначала мой декоратор "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>
        }
    }
}

Создать декоратор из интерфейса

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

И вот как я его использую:

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

Хитрость заключается в том, чтобы также создать интерфейс с тем же именем, что и у класса, для создания объединённой декларации.
Тем не менее класс отображается только как тип Test , но проверки из машинописного текста работают.
Если бы вы использовали декоратор без @ -Notation и просто вызвали бы его как функцию, вы бы получили правильный тип пересечения, но потеряли бы возможность проверки типов внутри самого класса, поскольку вы не можете использовать интерфейсный трюк уже не тот и выглядит более уродливо. Например:

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)

Что вы скажете об этих подходах? Это не красиво, но работает как обходной путь.

Есть ли прогресс в этом вопросе? Это кажется очень, очень мощной функцией, которая откроет множество различных возможностей. Я предполагаю, что эта проблема в основном устарела прямо сейчас, потому что декораторы все еще экспериментальны?

Было бы здорово получить официальное заявление об этом @andy-ms @ahejlsberg @sandersn. 🙏

Скорее всего, мы ничего не будем здесь делать, пока декораторы не будут завершены.

@DanielRosenwasser - что здесь означает «доработано»? Подходит ли этап 3 в TC39?

Я не знал, что они зашли так далеко! Когда они достигли стадии 3? Это недавно?

Они еще не сделали; Я считаю, что на январском собрании TC39 они снова готовы перейти со 2-го на 3-й этап.

Вы можете следить за повесткой дня для деталей.

Правильно (хотя я не могу подтвердить, что это будет продвигаться в январе). Я только спрашивал, пройдет ли этап 3, когда они туда доберутся.

На мой взгляд, в предложении декоратора все еще есть много серьезных проблем, поэтому, если оно перейдет к этапу 3 в январе, оно снова совершит ошибку, как и предложение проблемных полей класса и предложение globalThis.

@hax не могли бы вы уточнить?
Я действительно очень хотел бы этого и нахожу отсутствие связи по этому вопросу грустным и, к сожалению, не слышал о проблемах.

@AllNamesRTaken Проверьте список проблем с предложением декоратора. 😆 Например, export перед/после аргумента декоратора является блокировщиком этапа 3.

Также есть некоторые предлагаемые изменения, такие как редизайн API, которые, как я полагаю, означают, что предложение не является стабильным для этапа 3.

И это также связано с предложением проблемных полей класса. Хотя поля класса достигли стадии 3, проблем слишком много. Большая проблема, которую я беспокою, заключается в том, что многие проблемы остаются для декораторов (например, protected ), и это плохо для обоих предложений.

Обратите внимание, я не являюсь делегатом TC39, и некоторые делегаты TC39 никогда не соглашаются с моими комментариями о текущем состоянии многих вопросов. (В частности, у меня есть твердое мнение, что текущий процесс TC39 имеет большие неудачи по многим спорным вопросам. Отложите некоторые проблемы до тех пор, пока декораторы никогда не решат проблему, просто сделайте предложение декоратора более хрупким.)

Я думаю, что эти вещи будут решены, и предложение будет в порядке, но я бы предпочел, чтобы мы не обсуждали здесь состояние предложения.

Я думаю, что на этапе 3 с достаточной уверенностью или на этапе 4 мы, вероятно, реализовали бы новое предложение, а затем мы могли бы рассмотреть этот вопрос.

Спасибо Даниэль! Стадия 3 обычно подразумевает сильную уверенность в 4, так что скрестим пальцы перед январской встречей.

И спасибо, что остановили здесь обсуждение декораторов. Странно, какой уровень гнева и потери велосипеда вызвала эта функция. Я никогда не видел ничего подобного 😂

просто для записи был запрос функции о том же: https://github.com/Microsoft/TypeScript/issues/8545

достаточно раздражающе TypeScript поддерживает эту функцию в той степени, когда вы компилируете из JavaScript (https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#better-handling-for-namespace-patterns-in -js-файлы):

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

и даже для самого кода TypeScript, но только когда речь идет о функциях (!) (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 Есть новости о переходе предложения на этап 3? Я хочу создать библиотеку, которая добавляет функции прототипа в класс при использовании декоратора.

Они не продвинулись на последнем заседании TC39; они могут быть снова готовы к продвижению на следующем собрании. Однако это неясно; на данный момент много витает в воздухе о предложении.

Заинтересованным людям рекомендуется отслеживать само предложение — это уменьшит шум для тех из нас, кто следит за веткой, а также для сопровождающих TS.

какие-нибудь обновления по этому поводу?

обходной путь (угловой:)) 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
    }
}

Что вы думаете об этом «пока что» простом решении?
https://stackoverflow.com/a/55520697/1053872

Mobx-state-tree использует аналогичный подход со своей функцией cast() . Это неприятно, но... Меня это не слишком беспокоит. Его легко вынуть, когда появится что-то получше.

Я просто хотел бы иметь возможность сделать что-то в этом духе ❤️
Разве это не сила, которой должны обладать декораторы?

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

Я потратил довольно много времени на создание обходного пути для ввода декорированных свойств класса. Я не думаю, что это будет жизнеспособным решением для сценариев в этой теме, но вы можете найти некоторые его части полезными. Если вам интересно, вы можете проверить мой пост на Medium

Есть обновления по этому вопросу?

Как у нас с этим?

Каков результат этой проблемы, как это работает сейчас?

Вы можете использовать mixin-классы, чтобы получить этот эффект
https://mariusschulz.com/blog/mixin-classes-in-typescript

@Bnaya , что совершенно не то, чего мы сейчас хотим, не так ли ;).
Декораторы позволяют нам избежать создания классов, которые мы никогда не собираемся использовать, как мы делали это в старой школе Java. Декораторы позволили бы создать очень чистую и аккуратную архитектуру композиции, в которой класс может продолжать выполнять основные функции и иметь дополнительные обобщенные функции. Да, миксины могли бы это сделать, но это пластырь.

Предложение декоратора значительно изменилось за последние месяцы — крайне маловероятно, что команда TS будет тратить время на текущую реализацию, которая скоро станет несовместимой™.

Рекомендую посмотреть следующие ресурсы:

Предложение декораторов: https://github.com/tc39/proposal-decorators
Дорожная карта TypeScript: https://github.com/microsoft/TypeScript/wiki/Roadmap

@Bnaya , что совершенно не то, чего мы сейчас хотим, не так ли ;).
Декораторы позволяют нам избежать создания классов, которые мы никогда не собираемся использовать, как мы делали это в старой школе Java. Декораторы позволили бы создать очень чистую и аккуратную архитектуру композиции, в которой класс может продолжать выполнять основные функции и иметь дополнительные обобщенные функции. Да, миксины могли бы это сделать, но это пластырь.

Это не смешение, а класс.
Это не миксин плохого копирования, который
Когда появится оператор конвейера, синтаксис также будет разумным.

Вы можете использовать mixin-классы, чтобы получить этот эффект

Миксины класса-фабрики могут быть проблемой в TypeScript; даже если бы это было не так, они очень шаблонны, требуя, чтобы вы обернули классы в функцию для преобразования в миксин, что иногда вы не можете просто сделать, если вы импортируете сторонние классы.

Следующее намного лучше, проще, чище и работает с импортированными сторонними классами. У меня он отлично работает в простом JavaScript без транспиляции, кроме как для транспиляции декоратора в устаревшем стиле, но class es остаются полностью родными 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()

И вывод в Chrome:

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

Это полностью дает вам то, что делают миксины class-factory, без всего шаблонного. Обертки функций вокруг ваших классов больше не требуются.

Однако, как вы знаете, декоратор не может изменить тип определенного класса. 😢

Итак, на данный момент я могу сделать следующее, с той лишь оговоркой, что защищенные члены не наследуются:

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

Проблема с потерей защищенных членов иллюстрируется этими двумя примерами реализаций multiple (что касается типов, но реализация во время выполнения для краткости опущена):

  • пример без ошибки , потому что все свойства в объединенных классах являются общедоступными.
  • пример с ошибкой (в самом низу), потому что сопоставленные типы игнорируют защищенные члены, в этом случае Two.prototype.two не наследуется.

Мне бы очень хотелось найти способ сопоставления типов, включая защищенные члены.

Это было бы очень полезно для меня, потому что у меня есть декоратор, который позволяет вызывать класс как функцию.

Это работает:

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

Это не работает:

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

Обходной путь, который не так сложно очистить, когда появится эта функция, можно выполнить, используя слияние объявлений и файл определения пользовательских типов.

Имея уже это:

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

Добавьте следующий файл (файл будет удален после выхода функции):

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

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

Другой возможный обходной путь

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

Начато в 2015 году и до сих пор не завершено. Есть много других связанных проблем и даже статей, подобных этой https://medium.com/p/caf24aabcb59/responses/show , в которых пытаются показать обходные пути (которые немного хакерские, но полезные)

Может ли кто-нибудь пролить свет на это. Рассматривается ли это для внутреннего обсуждения?

tl;dr - эта функция затерта в TC39. Ни в коем случае нельзя винить в этом ТС. Они не собираются реализовывать, пока это не станет стандартом.

Но это Microsoft, они могут просто внедрить, а потом сказать нормовщикам - "видите, народ уже пользуется нашей версией" :trollface:

Проблема здесь в том, что предложение декораторов изменилось _значительно_ (обратно несовместимым образом) с тех пор, как TypeScript реализовал декораторы. Это приведет к болезненной ситуации, когда новые декораторы будут завершены и реализованы, поскольку некоторые люди полагаются на текущее поведение и семантику декораторов TypeScript. Я сильно подозреваю, что команда TypeScript хочет избежать решения этой проблемы, потому что это будет стимулировать дальнейшее использование текущих нестандартных декораторов, что в конечном итоге сделает переход к любой новой реализации декоратора еще более болезненным. По сути, я лично просто не вижу, чтобы это происходило.

Между прочим, я недавно создал #36348, который представляет собой предложение функции, которая обеспечит довольно надежное обходное решение. Не стесняйтесь взглянуть на/оставить отзыв/поругать его. 🙂

tl;dr - эта функция затерта в TC39. Ни в коем случае нельзя винить в этом ТС. Они не собираются реализовывать, пока это не станет стандартом.

Учитывая этот факт, может быть разумно, чтобы команда разработчиков написала краткое объяснение и обновление статуса и заблокировала этот разговор, пока TC39 не переместит его на требуемый этап?

Кто-нибудь из читающих это имеет какой-либо вес в этом вопросе?

На самом деле, почти каждый разговор, связанный с декораторами, висит в воздухе. Пример: https://github.com/Microsoft/TypeScript/issues/2607 .

Получение некоторой ясности относительно будущего декораторов будет полезно, даже если речь идет о том, чтобы попросить участников реализовать требуемую функциональность. Без четкого руководства будущее декораторов размыто

Я был бы рад, если бы команда TypeScript могла взять на себя активную роль в предложении декоратора, подобно тому, как они помогли реализовать необязательную цепочку. Я также хотел бы помочь, но еще не занимался разработкой языков и поэтому не знаю, как лучше всего это сделать :-/

Я не могу говорить за всю команду и TypeScript как проект, но я думаю, что маловероятно, что мы будем делать какую-либо новую работу по декоратору, пока она не будет решена и подтверждена, учитывая, что реализация декоратора уже однажды отличалась от реализации TypeScript.

Предложение все еще находится в активной разработке и появилось на TC39 на этой неделе https://github.com/tc39/proposal-decorators/issues/305 .

@orta в этом случае я думаю, что тогда следует обновить документы о декораторе классов . В нем говорится, что

Если декоратор класса возвращает значение, он заменяет объявление класса предоставленной функцией-конструктором.

Кроме того, следующий пример из документации, по-видимому, подразумевает, что тип экземпляра Greeter будет иметь свойство newProperty , что неверно:

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

Думаю, стоит добавить в документы, что интерфейс возвращаемого класса не заменит оригинальный.

Интересно, я протестировал кучу старых версий TypeScript и не смог найти случая, когда этот пример кода работал ( пример из 3.3.3 ) — так что да, я согласен.

Я сделал https://github.com/microsoft/TypeScript-Website/issues/443 - если кто-то знает, как сделать, чтобы этот classDecorator имел явный тип пересечения, пожалуйста, прокомментируйте эту проблему

Я оказался здесь, потому что проблема с перечисленной выше документацией вызвала у меня замешательство. Хотя было бы здорово, если бы сигнатура типа возвращаемого декоратором значения распознавалась компилятором TS, вместо этого большего изменения я согласен с тем, что документацию следует уточнить.

Для справки, это упрощенная версия того, что я пытался сделать в формате соответствующих документов из PR @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);
Была ли эта страница полезной?
0 / 5 - 0 рейтинги