Typescript: 请求:类装饰器突变

创建于 2015-09-20  ·  231评论  ·  资料来源: microsoft/TypeScript

如果我们可以正确地进行类型检查,我们将对无样板的 mixin 提供完美的支持:

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 ,这正是我们需要的! 它会让 mixins 很自然地使用。

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

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

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

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

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

为此+1。 虽然我知道这很难实现,而且可能更难就装饰器突变语义达成协议。

+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_ 属性也很有用(例如,为 getter 生成关联的 setter 的帮助器,或类似的东西)

connect的 react-redux 类型需要一个组件并返回一个修改过的组件,其道具不包括通过 redux 接收到的连接道具,但似乎 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 应用程序转换为 typescript 并点击它)。

@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

那些let语句范围内的 $#$ Foo $#$ 已被装饰器更改。 原来的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();

它生成测试/基线/本地/decoratorOnClass10.types

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

class Y {}
>Y : Y

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

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

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

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

export default class C {
>C : C
}

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

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

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

我期待
>C: typeof C>C: typeof X | typeof Y

对于那些对 react-redux 的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 已经发布,并且该语言支持 mixin,这是一个非常有用的功能。

@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 mixin:

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的实例有superPropertyloggersubProperty

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

您还可以使用 mixin 作为返回要使用的具体类的表达式:

const LoggableFoo = Loggable(Foo);

您_可以_也使用类 mixin 作为装饰器,但它的语义略有不同,主要是子类化您的类,而不是允许您的类继承它。

类 mixins 比装饰器有几个优点,IMO:

  1. 他们创建了一个新的超类,以便您应用它们的类可以更改以覆盖它们
  2. 他们现在键入检查,没有任何来自 TypeScript 的附加功能
  3. 它们适用于类型推断 - 您不必输入 mixin 函数的返回值
  4. 它们适用于静态分析,尤其是跳转到定义 - 跳转到logger的实现会将您带到 mixin _implementation_,而不是接口。

@justinfagnani我什至没有考虑过混入,所以谢谢。 今晚我将继续编写一个Loggable混合程序,以使我的 Logger 附件语法更好一些。 extends Mixin(SuperClass)路线是我的首选,因为它是自 TS 2.2 发布以来我使用 mixins 的方式。

但是,我确实更喜欢装饰器语法的想法而不是 mixins,所以我仍然希望可以为这个特定问题提供一些解决方案。 在我看来,能够使用装饰器创建无样板代码的 mixin 将对更简洁的代码大有裨益。

@zajrik很高兴这个建议有帮助,我希望

我仍然不太明白 mixins 如何比装饰器有更多的样板。 它们的句法权重几乎相同:

混合类:

class LoggableFoo extends Loggable(Foo) {}

与装饰者:

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

在我看来,mixin 的意图更清楚:它正在生成一个超类,而超类定义了一个类的成员,所以 mixin 可能也定义了成员。

装饰器将用于很多事情,你不能假设它是或不是定义成员。 它可能只是为某些东西注册类,或者将一些元数据与它相关联。

公平地说,我认为@zajrik想要的是:

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

不可否认,即使如此轻微,也少了样板文件。

也就是说,我喜欢 mixin 解决方案。 我一直忘记混入是一回事。

如果您关心的只是向当前类添加属性,那么 mixins 基本上相当于具有一个显着烦恼的装饰器……如果您的类还没有超类,则需要创建一个空的超类来使用它们。 总体而言,语法似乎更重。 此外,尚不清楚是否支持参数混合(是否允许extends Mixin(Class, { ... }) )。

@justinfagnani在您的原因列表中,第 2-4 点实际上是 TypeScript 的缺陷,而不是 mixins 的优势。 它们不适用于 JS 世界。

我认为我们都应该清楚,基于 mixin 的 OP 问题解决方案将涉及向原型链添加两个类,其中一个是无用的。 这反映了 mixins 与装饰器的语义差异,但是 mixins 给了你一个拦截父类链的机会。 然而 95% 的时间这不是人们想要做的,他们想要装饰这个类。 虽然 mixins 的用途有限,但我认为将它们作为装饰器和高阶类的替代品在语义上是不合适的。

Mixins 基本上等同于有一个显着烦恼的装饰器......如果你的类还没有超类,你需要创建一个空的超类来使用它们

这不一定是真的:

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

class Foo extends Mixin() {}

总体而言,语法似乎更重。

我只是不明白这是怎么回事,所以我们不得不不同意。

此外,尚不清楚是否支持参数 mixin(是否允许 extends Mixin(Class, { ... }) )。

他们非常喜欢。 Mixin 只是函数。

在您的原因列表中,第 2-4 点实际上是 TypeScript 的缺陷,而不是 mixins 的优点。 它们不适用于 JS 世界。

这是一个 TypeScript 问题,因此它们适用于此。 在 JS 世界中,装饰器实际上还不存在。

我认为我们都应该清楚,基于 mixin 的 OP 问题解决方案将涉及向原型链添加两个类,其中一个是无用的。

我不清楚你从哪里得到两个。 它是一个,就像装饰者可能做的那样,除非它正在修补。 哪个原型没用? mixin 应用程序大概添加了一个属性/方法,这不是没用的。

这反映了 mixins 与装饰器的语义差异,但是 mixins 给了你一个拦截父类链的机会。 然而 95% 的时间这不是人们想要做的,他们想要装饰这个类。

我不太确定这是真的。 通常在定义一个类时,您希望它位于继承层次结构的底部,具有覆盖超类方法的能力。 装饰器要么必须修补有许多问题的类,包括不使用super() ,要么扩展它,在这种情况下,装饰类无法覆盖扩展。 这在某些情况下可能很有用,例如覆盖类的每个已定义方法以进行性能/调试跟踪的装饰器,但它与通常的继承模型相去甚远。

虽然 mixins 的用途有限,但我认为将它们作为装饰器和高阶类的替代品在语义上是不合适的。

当开发人员想要向原型链中添加成员时,mixin 在语义上是完全合适的。 在我看到有人想为 mixins 使用装饰器的每种情况下,使用类 mixins 将完成相同的任务,具有他们实际上期望装饰器的语义,由于使用 super 调用的工作属性而具有更大的灵活性,以及当然他们现在工作。

当 Mixin 直接处理用例时,它们几乎是不合适的。

当开发人员想要将成员添加到原型链时

这正是我的观点,OP 不想在原型链中添加任何东西。 他只想改变一个类,而且大多数情况下,当人们使用装饰器时,他们甚至没有除 Object 之外的父类。 并且由于某种原因Mixin(Object)在 TypeScript 中不起作用,因此您必须添加一个虚拟的空类。 所以现在你有一个原型链 2(不包括 Object)当你不需要它时。 另外,将新类添加到原型链中需要付出不小的代价。

至于语法比较Mixin1(Mixin2(Mixin3(Object, { ... }), {... }), {...}) 。 每个 mixin 的参数都尽可能远离 mixin 类。 装饰器语法显然更具可读性。

虽然装饰器语法本身不进行类型检查,但您可以使用常规函数调用来获得您想要的内容:

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是的,我已经尝试过@justinfagnani的建议,即过去在 TypeScript 2.2 中使用带有默认Object参数的 mixin,并且 tsc 拒绝了代码。

@ohjames它仍然有效,您只需要注意默认情况下的原型(请参阅该常见问题解答条目)。

不过,通过null通常更容易依赖tslib.__extend的行为

有没有计划在下一个迭代步骤中关注这个问题? 此功能的好处是在如此多的库中都非常高。

我刚刚遇到了这个问题——它迫使我编写了很多不需要的代码。 解决这个问题对任何基于装饰器的框架/库都有很大的帮助。

@TomMarius正如我之前提到的,包装在装饰器函数中的类已经正确地进行类型检查,您只是不能使用@语法糖。 而不是这样做:

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

你只需要这样做:

const Foo = loggable(class { });

您甚至可以在将类包装到其中之前将一堆装饰器函数组合在一起。 虽然使语法糖正常工作很有价值,但这似乎不应该是一个如此巨大的痛点。

@masaeedu真的问题不是外部的,而是内部的类型支持。 至少对我来说,能够使用装饰器在类本身中添加的属性而不会出现编译错误是期望的结果。 您提供的示例只会为Foo提供可记录类型,但不会为类定义本身提供类型。

@zajrik装饰器从原始类返回一个类,即使您使用内置的@语法。 显然 JS 并不强制要求纯度,所以你可以自由地改变你传递的原始类,但这与装饰器概念的惯用用法不一致。 如果您将通过装饰器添加到类内部的功能紧密耦合,那么它们也可能是内部属性。

你能给我一个在稍后通过装饰器添加的类内部使用 API 的用例示例吗?

上面的 Logger 示例是能够操纵装饰类内部的常见_want_ 的一个很好的示例。 (并且对于来自其他具有类装饰的语言的人来说很熟悉;例如 Python

也就是说, @justinfagnani类 mixin 建议对于这种情况似乎是一个不错的选择

如果您希望能够定义类的内部结构,那么执行此操作的结构化方法不是修补类或定义新的子类,TypeScript 将很难在类的上下文中进行推理本身,但要么只是在类本身中定义事物,要么创建一个具有所需属性的新超类,TypeScript 可以推理。

装饰器真的不应该以对类或大多数消费者可见的方式改变类的形状。 @masaeedu就在这里。

虽然您说的是真的,但 TypeScript 并不是为了强制执行干净的编码实践,而是为了正确键入 JavaScript 代码,在这种情况下它失败了。

@masaeedu @zajrik说了什么。 我有一个装饰器,它声明了一个“在线服务”,它将一堆属性添加到一个类中,然后在该类中使用。 由于缺少元数据和约束强制(如果您力求不重复代码),子类化或实现接口不是一种选择。

@TomMarius我的意思是它是正确的类型检查。 当您将装饰器函数应用于类时,该类不会以任何方式更改。 对原类进行一些改造,产生一个新的类,只有这个新类才能保证支持装饰器函数引入的API。

我不知道“缺少元数据和强制执行”是什么意思(也许一个具体的例子会有所帮助),但如果你的类显式依赖于装饰器引入的 API,它应该直接通过@justinfagnani显示的 mixin 模式对其进行子类化,或通过构造函数或其他东西注入它。 装饰器的实用性在于它们允许对那些对修改关闭的类进行扩展,以使使用这些类的代码受益。 如果您可以自己定义类,只需使用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(); } }

您可以清楚地看到基于继承的方法示例中的代码重复。 在我和我的用户身上发生过很多次,当他们复制粘贴类或开始使用“any”作为类型参数并且库停止为他们工作时,他们忘记了更改类型参数; 基于装饰器的方法对开发人员更加友好。

在那之后,基于继承的方法还有另一个问题:现在缺少反射元数据,因此您必须更多地复制代码,因为无论如何您都必须引入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的返回类型作为thisfoo的类型,才能在简洁性方面取得巨大的胜利。 更重要的是,你打算如何为此提出一个更合理的打字策略? 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)

--
我认为装饰器的返回类型不应该出现在课堂上。
因为:.... emmm。 不知道怎么形容,对不起我的英语水平不好...(🤔没有相框的照片可以存在吗?)这份工作是为了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());

因此,如果装饰器定义的方法已经存在并且可以合法调用,那么如果 typescript 反映了这一点,那就太好了。

希望打字稿在不强加变通方法的情况下反映这一点。 如果还有其他
装饰器可以做的事情不能被建模,那么如果这个至少会很好
场景以某种形式或形式得到支持。

这个线程充满了关于装饰器该做什么、应该如何使用、如何应该使用等等的意见,令人作呕

这就是装饰器实际做的事情:

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从下到上应用每个装饰器,每个装饰器都可以选择返回一个全新的类。

我理解让类型系统支持并承认这可能很棘手,而且我理解支持这一点可能需要时间,但我们不能都同意当前的行为,从上面的原始代码示例开始线程,根本不正确?

无论任何开发人员对装饰器、mixin 或功能组合等有什么看法,这似乎都是一个非常明显的错误。


我可以礼貌地询问这是否恰好在未来发布的准备中?

TypeScript 简直太棒了,我喜欢它; 然而,这似乎是少数几个(唯一?)被简单破坏的部分之一,我只是期待看到它修复:)

@arackaf很好理解装饰器的实际作用,假设您不关心类型,TypeScript 的 emit 已经支持装饰器。 这个问题的整个争论是关于装饰类应该如何在类型系统中呈现。

我同意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。 因此,logger 装饰器将 Foo 视为 Greeter 应该是有效的,但对其他人无效,因为 Foo 被装饰器重新分配给完全不同的东西。

我敢肯定,实施这将非常困难。 对于此线程顶部列出的常见情况,诸如接口合并之类的变通方法是此类棘手边缘情况的后备,是否可能有一些合理的子集?

@arackaf

然而 Foo 然后被装饰器突变为完全不同的东西

首先,对于Foo是什么没有精确的定义。 请记住,我们的全部争论点是Foo的成员是否应该能够访问(并因此作为公共 API 的一部分返回),装饰器从this引入了成员。 Foo是什么由装饰器返回的内容定义,而装饰器返回的内容由Foo是什么定义。 它们是密不可分的。

我敢肯定,实施这将非常困难

现在谈论实现复杂性还为时过早,因为我们甚至没有关于该功能应该如何工作的可靠建议。 很抱歉,我们需要有人提出一个具体的机制来解决“当我将这样一个装饰器序列应用于这样的类时, this会发生什么”。 然后我们可以在不同的部分插入各种 TypeScript 概念,看看结果是否有意义。 只有这样讨论实现复杂性才有意义。

我在上面粘贴的当前装饰器输出是否不符合规范? 我认为这是我所看到的。

假设是这样,Foo 在每一步都有相当精确的含义。 我希望 TS 允许我根据最后一个装饰器返回的内容使用this (Foo 方法内部和 Foo 实例),如果装饰器返回,TS 会强制我沿途添加类型注释功能不够可分析。

@arackaf没有“步骤”; 对于给定的不可变代码片段,我们只关心this的最终类型。 您需要至少在较高的层次上描述您期望this的类型应该是用装饰函数f1...fn装饰的类X的成员,用术语Xf1...fn的类型签名。 您可以在此过程中执行任意多个步骤。 到目前为止,没有人这样做。 我一直在猜测人们的意思是装饰器的返回类型应该呈现为this的类型,但据我所知,我可能完全不合时宜。

如果您的建议是机械地使类型反映转译输出中的值发生的情况,那么您最终会得到我建议的内容而不是您建议的内容:即new Foo().hello()很好,但是this.hello()不是。 在该示例中,您正在装饰的原始类没有获得hello方法。 只有__decorate([addMethod], Foo)的结果(然后分配给Foo )具有hello方法。

我一直在猜测人们的意思是装饰器的返回类型应该呈现为 this 的类型

哦,对不起,是的,没错。 正是这样。 句号。 因为这就是装饰者所做的。 如果行中的最后一个装饰器返回一些全新的类,那么这就是 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的类型兼容。

你可以用这个产生任何数量的荒谬。 假设我暂停了关于作为c的结果键入this的循环性的反对意见,这是由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,它会调用装饰类的bar

但是说起来应该是错误的

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

因为 Foo 的 hello 方法现在根据 Logger 返回的类返回 void。

当然 - 但我说调用new Foo().foo()是错误的

@arackaf但这没关系。 我没有调用new Foo().foo() 。 我调用this.foo() ,我得到了一个类型错误,即使我的代码在运行时运行良好

但是说let s : string = new Foo().hello()应该是错误的

同样,这无关紧要。 我并不是说Foo.prototype.hello的最终类型应该是() => string (我同意它应该是() => void )。 我抱怨有效调用this.bar()出错了,因为您已经通过手术移植了一种类型,而移植它是荒谬的。

这里有两个Foo。 当你说

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() WITHIN 原始 Foo 不是类型错误 - 这对于曾经绑定到标识符Foo的(现在无法访问的)类有效。

这是一个特殊的、有趣的琐事,但我不明白为什么这会阻止 TS 处理变异的类装饰器。

@arackaf你没有回答这个问题。 具体来说, this的类型是什么? 您不能只是无休止地循环回答“ this是 Foo 而 Foo 是this ”。 this有哪些成员? 如果它有除hello(): void之外的任何成员,使用什么逻辑来确定它们?

当你说“ this.foo() WITHIN the original 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 ,因为这是 Class 的类型(扩展 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(); }不是 TypeError - 为什么会这样? 仅仅因为该类定义不再可访问并不会以某种方式使该方法无效。

我不希望 TypeScript 能够完美地处理这些边缘情况。 像往常一样,必须在这里和那里添加类型注释是可以理解的。 但是这些例子都没有说明为什么@logger不能从根本上改变Foo的绑定。

如果logger是一个返回新类的函数,那么这就是 Foo 现在引用的。

我已经回答了所有关于这是什么以及在哪里的问题。
简单的事实是, this的含义会根据您所在的位置而变化。

这真是令人沮丧。 好吧,它改变了,它是Foo ,它是一个静态绑定等等等等。什么是类型签名? this有哪些成员? 你说的是阳光下的一切,而我所需要的只是thishello中的简单类型签名。

new LoggableFoo().hello()碰巧调用了一个 void 方法,该方法碰巧调用了一个通过 Greeter 键入的方法,否则无法访问。

这与不可访问不同。 当您忽略可到达的路径时,每个可到达的方法都是“否则无法到达的”。

如果你做了:

但这就是所做的。 这正是我的代码片段,再次粘贴,前面是我为您构建的示例的解释。 我什至通过差异检查器对其进行了检查,以确保我没有服用疯狂的药丸,唯一的区别是您删除的评论。

再说一次, hello() { return this.foo(); }不是 TypeError - 为什么会这样?

因为您(和这里的其他人)希望this的类型是装饰器的实例化返回类型,即{ hello(): void } (请注意缺少foo成员)。 如果您希望Foo的成员能够看到this作为装饰器的返回类型,则hellothis的类型将为{ 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 是一个类型错误是一个错误,并且很容易修复。我不同意返回 this.foo;作为一个类型错误是一个错误”。 类似地,在您的示例中,我同意new Foo().x()是一个类型错误是一个错误,但this.x()是一个类型错误不是。

您看到此页面顶部的代码段中有两条评论吗?

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

^ 那个是我觉得有问题的那个。 要么你同意装饰器的返回类型不应该出现在this上,而只能出现在new Foo()上,在这种情况下,我们都没有争论。 或者您不同意并且也想要该功能,在这种情况下,您之前评论中的片段是无关紧要的。

我终于明白你的意思了。 我很难从你的 Greeter 代码中得到这个,但我现在正在跟踪; 谢谢你这么有耐心。

我想说唯一明智的解决方案是让 Foo(Foo 内部)支持原始 Foo 的类型联合(derp 我的意思是交集),以及最后一个装饰器返回的任何内容。 对于像 Greeter 这样的疯狂示例,您必须支持原始的Foo ,并且您肯定需要支持最后一个装饰器返回的任何内容,因为这是使用装饰器的全部意义(根据上面的许多评论)。

所以是的,从我最近的例子来看,在Foo x、y、z、a、b、c 内部都可以工作。 如果有两个版本的a ,那么都支持。

@arackaf np,也感谢您的耐心等待。 我的例子并不是最清楚的,因为我只是发布了我可以在操场上做的任何东西来展示它是如何被破坏的。 我很难系统地思考它。

我想说唯一明智的解决方案是让 Foo(在 Foo 内部)支持原始 Foo 的类型联合,以及最后一个装饰器返回的任何内容。

好的太棒了,所以我们现在进入它的细节。 如果我弄错了,请纠正我,但是当您说“联合”时,您的意思是它应该具有两者的类型成员,即它应该是A & B 。 所以我们希望thistypeof(new OriginalClass()) & typeof(new (decorators(OriginalClass))) ,其中decorators是所有装饰器的组合类型签名。 简单来说,我们希望this是“原始类”的实例化类型和通过所有装饰器的“原始类”的实例化类型的交集。

这有两个问题。 一个是在像我的示例这样的情况下,这只允许您访问不存在的成员。 我可以在装饰器中添加一堆成员,但如果我尝试在我的类中使用this.newMethod()访问它们,它只会在运行时呕吐。 newMethod仅添加到装饰器函数返回的类中,原始类的成员无法访问它(除非我特别碰巧使用了return class extends OriginalClass { newMethod() { } }模式)。

另一个问题是“原始类”不是一个定义明确的概念。 如果我可以从this访问修饰成员,我也可以将它们用作返回语句的一部分,因此它们可能是“原始类”公共 API 的一部分。 我在这里有点挥手,我有点筋疲力尽,无法想到具体的例子,但我认为如果我们认真思考,我们可以提出荒谬的例子。 也许您可以通过找到某种方法来解决此问题,以隔离不返回他们从this访问的内容的成员,或者至少不会因为返回this.something()而推断返回类型的成员

@masaeedu是的,在您回复之前,我在联合/交叉点上更正了自己。 对于刚接触 TS 的人来说,这是违反直觉的。

其余的就明白了。 老实说,装饰器通常不会返回完全不同的类型,它们通常只会增加传入的类型,因此交集的东西在大多数情况下都会“正常工作”。

我想说您谈论的运行时错误很少见,并且是一些故意错误的开发决策的结果。 我不确定您是否真的需要关心这一点,但是,如果这确实是一个问题,我会说仅使用最后一个装饰器返回的内容将是一个不错的第二位(所以是的,一个类可以通过尝试使用自己定义的方法 - 不理想,但仍然值得为装饰器工作付出代价)。

但实际上,我认为您正在考虑的运行时错误不值得预防,当然是以装饰器正常工作为代价的。 此外,如果你粗心或愚蠢,很容易在 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
    }
}

让它正常工作,并为 blah 和装饰器添加的任何其他内容提供一流的支持。 对于那些做疯狂事情的​​用例,比如返回一个全新的类(比如你的 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() { }
    };
}

在扩展子句中应该可以正常工作:

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

在类型系统中使用更简单且已经定义的语义。 这已经奏效了。 您试图通过子类声明的类来解决什么问题,而不是为它创建一个超类?

@masaeedu

装饰器今天的工作方式并不能证明在一般情况下改变这种情况是合理的。

(类)装饰器的主要用途绝对是积极地以一种或另一种方式改变类中this的值。 Redux 的@connect和 MobX 的@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 (Anonymous class)' 不能分配给类型 'T & (new () => { blah(): void; })'。
类型“typeof(匿名类)”不可分配给类型“T”。

在TS游乐场。 不确定我是否有一些选项设置错误。

您试图通过子类声明的类来解决什么问题,而不是为它创建一个超类?

我只是想使用装饰器。 我很高兴在 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 是否没有注释可以告诉我们在上面的示例中添加abc以便类型系统处理前者和后者没有区别?

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

我只是想使用装饰器。

这并不是您真正要解决的问题,而是重言式。 就像问:“用那把锤子想做什么?” 答:“只需使用锤子”。

您是否想到了一个实际案例,其中生成具有预期扩展的新超类不起作用? 因为 TypeScript 现在支持这一点,正如我所说,在这种情况下,从类内部推断类的类型要容易得多。

我查看了 Mobx 的@observer注释,它看起来并没有改变类的形状(并且很容易成为方法装饰器而不是类装饰器,因为它只包装render() )。

@connect实际上是正确键入装饰器的复杂性的一个很好的例子,因为@connect似乎甚至没有返回装饰类的子类,而是包装了装饰类的全新类,然而静态数据被复制过来,因此结果丢弃了实例端接口并保留了 TS 在大多数情况下甚至不检查的静态端。 看起来@connect根本不应该是装饰器,而 connect 应该只是用作 HOC,因为作为装饰器,它非常令人困惑。

一年多以来,我一直很高兴地在 JS 中使用它们

嗯……你真的没有。 JS 没有装饰器。 您可能一直在使用一个特定的已失效的先前提案,该提案在 Babel 和 TypeScript 中的实现方式略有不同。 在其他场合,我主张装饰师继续标准化进程,但进展已经放缓,我认为这还不是确定的。 许多人认为它们不应该被添加,或者只是注释,所以语义甚至可能仍然悬而未决。

对装饰器的抱怨之一就是它们可以改变类的形状,使得静态推理变得困难,并且已经讨论了一些建议,至少限制装饰器这样做的能力,尽管我认为那是在最近的提案获得了规范语言。 我个人认为表现良好的装饰器_不应该_改变类的形状,以便出于静态分析的目的可以忽略它们。 请记住,标准 JS 没有可以依靠的类型系统来告诉分析函数实现在做什么。

  • 我只是渴望TS赶上来。

我看不出 TypeScript 是如何落后的。 至少,背后是什么? 装饰师确实工作。 是否有另一种类型化的 JS,其中类的行为方式是你想要的?

装饰器现在是第 2 阶段,几乎在每个 JS 框架中都使用。

至于@connect ,react-redux 每月的下载量约为 250 万次; 听到设计在某种程度上“错误”是令人难以置信的傲慢,因为你会以不同的方式实现它。

当前尝试使用装饰器引入的方法会导致 TypeScript 中出现类型错误,需要解决方法,例如接口合并、手动为添加的方法添加注释,或者只是不使用装饰器来改变类(好像任何人都需要被告知我们可以只是不要使用装饰器)。

真的,真的没有办法手动,煞费苦心地通知 TS 编译器装饰器正在改变类的形状吗? 也许这很疯狂,但 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 每月的下载量约为 250 万次; 听到设计在某种程度上“错误”是令人难以置信的傲慢,因为你会以不同的方式实现它。

请不要说我傲慢等人身攻击。

  1. 我没有人身攻击你,所以不要人身攻击我。
  2. 它根本无助于你的论点。
  3. 一个项目的受欢迎程度不应将其排除在技术评论之外。
  4. 我们在谈论同一件事吗? redux-connect-decorator 每天有 4 次下载。 react-redux 的connect()函数看起来像一个 HOC,就像我建议的那样。
  5. 我认为我认为行为良好的装饰器不应该改变一个类的公共形式,当然应该用一个不相关的类来代替它,这是相当合理的,并且会在 JS 社区中找到足够的共识。 即使你不同意,这也远非傲慢。

当前尝试使用装饰器引入的方法会导致 TypeScript 中出现类型错误,需要解决方法,例如接口合并、手动为添加的方法添加注释,或者只是不使用装饰器来改变类(好像任何人都需要被告知我们可以只是不要使用装饰器)。

这并不是说我们只是建议根本不使用装饰器,而是我相信@masaeedu是在说,为了以类型安全的方式修改原型,我们有现有的机制:继承和 mixin 应用程序。 我试图问为什么您的d示例不适合用作mixin,而您没有给出答案,只是说您只是想使用装饰器。

我想这很好,但似乎相当严格地限制了对话,所以我会再次鞠躬。

@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所做的事情,它只使用自己的propsstate 。 因此,从这个意义上说, 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 库会接收一个类,然后用新方法吐出一个新类,这些新类将在装饰类中使用。 这是我和其他人经常做的事情,在我们自己的代码中,以实现我们自己的 mixin,但我真的没有关于 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子句中应该有满足要求的东西。 这就是它一直以来的工作方式,即使使用 decorators 也是如此。 如果你有一个装饰器,它添加了一个类所依赖的一些功能,那么这个类应该扩展装饰器返回的任何东西,而不是相反。 这只是常识。

我不确定为什么这是常识。 这是目前我们代码库中的代码。 我们有一个装饰器,它向类的原型添加一个方法。 TypeScript 错误。 当时我只是把这个声明扔在那里。 我本可以使用接口合并。

但是什么都不用就好了。 能够告诉 TypeScript “这个装饰器将这个方法添加到它应用到的任何类中 - 也允许类中的this访问它”,这将是非常非常好的

这个线程中的许多其他人似乎也在使用类似的装饰器,所以我希望你会考虑到这不应该工作可能不是常识。

image

@arackaf是的,所以你应该这样做export class SearchVm extends (@mappable({...}) class extends SearchVmBase {})SearchVm取决于可映射性,而不是相反。

导出类 SearchVm 扩展 (@mappable({...}) 类扩展 SearchVmBase {})

这个线程的重点是避免像这样的样板。 装饰器提供的 DX 比必须做的事情要好得多。 我们选择像我一样编写代码是有原因的,而不是那样。

如果你通读这个帖子的评论,我希望你会相信许多其他人正在尝试使用更简单的装饰器语法,因此重新考虑我们“应该”做什么,或者什么是“常识”等。

您想要的与装饰器无关。 你的问题是 TypeScript 的机制说“嘿 TypeScript,这个结构正在发生你一无所知的事情,让我告诉你一切”,目前太有限了。 现在我们只有接口合并,但你希望函数能够将接口合并应用于它们的参数,这与装饰器没有任何关系。

现在我们只有接口合并,但你希望函数能够将接口合并应用于它们的参数,这与装饰器没有任何关系。

行。 很公平。 你要我打开一个单独的问题,还是要重命名这个问题,等等?

@arackaf唯一的“样板”是我有class { }的事实,这是a)固定的7个字符,无论您使用多少个装饰器b)装饰器不能(还) 应用于任意返回类的表达式,以及c)因为我特别想为您的利益使用装饰器。 这种替代配方:

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

并不比你最初所做的更冗长。

行。 很公平。 你要我打开一个单独的问题,还是要重命名这个问题,等等?

单独的问题会很好。

并不比你最初做的更冗长

它不再冗长,但我真的希望你能考虑到许多人根本不喜欢这种语法的事实。 无论好坏,许多人更喜欢 DX 装饰器提供的服务。

单独的问题会很好。

我明天会输入一个,并链接到这个以获取上下文。

感谢您帮助解决这个问题:)

如果我可以进入, @masaeedu我不同意这种说法:

您想要的与装饰器无关。 你的问题是 TypeScript 的机制说“嘿 TypeScript,这个结构正在发生你一无所知的事情,让我告诉你一切”,目前太有限了。 现在我们只有接口合并,但你希望函数能够将接口合并应用于它们的参数,这与装饰器没有任何关系。

当然,TypeScript 可以查看类装饰器的返回类型来确定变异类应该是什么类型。 不需要“将接口合并到参数”。 所以在我看来,这完全与装饰器有关。

此外,TypeScript 不知道连接的组件不再需要 props 的情况是让我拒绝使用 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 都可以简单地从装饰器结果中推断出实际类型,以我的拙见。

如果您感到屈尊,请道歉; 目的是让你
实际上是一个巨大的杂乱无章的线程,任何人都可以原谅
略读。 我支持这一点,因为很明显你没有意识到
的细节,并且基于对我的误解而与我“不同意”
我的位置。

例如,您关于为什么接口合并功能的问题
论据解决了亚当的问题,最好通过查看相关的
与 Adam 讨论,他说他已经在使用界面合并,
但发现在每个申报地点都必须这样做很烦人。

我认为这一切的技术方面都被打死了。 一世
不希望线程被人际争吵所支配,所以这个
将是我的最后一篇文章。

确实,装饰器有两个错误:不尊重返回类型,以及在类内部,不尊重添加的成员(类内部的this )。 是的,我更关心后者,因为前者更容易解决。

我在这里打开了一个单独的案例: https ://github.com/Microsoft/TypeScript/issues/16599

大家好,我在周末错过了这次谈话,也许现在没有太多理由把它重新提起; 但我想提醒大家,在这类讨论的热烈讨论中,参与讨论的每个人通常都是善意的。 保持尊重的语气有助于澄清和引导对话,并可以培养新的贡献者。 这样做并不总是那么容易(尤其是当互联网将音调留给读者时),但我认为这对未来的场景很重要。 😃

这方面的现状如何?

@alex94puchades仍然是第 2 阶段的提案,所以我们可能还有一段时间。 至少,TC39似乎有一些动静

根据此评论,看起来最早可以在 11 月为第 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 模态的东西。

假设我有一个可以在自定义ConfirmModalComponent上使用的@ModalParams(...)装饰器。 为了在我的自定义组件中显示@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 这样的框架当然会大量使用装饰器来扩充类,但设计并不是让类“获得”或混合来自装饰器的新逻辑,然后它可以在自己的实现中使用这些逻辑——它是为了隔离来自框架协调逻辑的类逻辑。 无论如何,这是我的_humble_意见。 : - )

只使用高阶类而不是装饰器 + hacks 似乎更好。 我知道人们想为这些东西使用装饰器,但使用 compose + HOCs 是可行的方法,它可能会一直保持这种方式......永远;) 根据 MS 等。装饰器仅用于将元数据附加到类,当你检查 Angular 等装饰器的大用户,您会发现它们仅用于此功能。 我怀疑你能否说服 TypeScript 维护者。

如此强大的功能,允许真正的功能组合,并产生这样的参与度,现在被 TS 团队忽视了这么长时间,这令人悲伤和有点奇怪。

这确实是一个可以彻底改变我们编写代码的功能; 允许每个人在 Bitly 或 NPM 上发布小型 mixin,并在 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);
    }
}

然后你甚至不需要一个 init 函数,你可以这样做:

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

@lansana
是的,但这会污染我的构造函数,这并不总是我想要的超级骗子。

我也有一个类似的 mixin 来使任何对象 Poolable 和其他东西。 只是需要通过组合添加功能的可能性。 init 示例只是在不污染构造函数的情况下完成您所做的事情的一种必要方式,使其可用于其他框架。

@AllNamesRTaken现在接触装饰器是没有意义的,因为它已经处于这种状态很长时间了,并且提案已经处于第 2 阶段。 等待https://github.com/tc39/proposal-decorators完成,然后我们很可能会以每个人都同意的任何形式获得它们。

@Kukkimonsuta
我强烈反对你,因为我要求的纯粹是关于类型而不是功能。 因此,它与 ES 的东西没有太多关系。 我上面的解决方案已经有效,我只是不想强制转换为.

@AllNamesRTaken您可以在_today_ 使用 mixins 执行此操作,而不会发出任何警告:

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

将来,如果我们将 mixins 提案导入 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 mixin 是我开始使用这些功能的方式,我的实现实际上也像你描述的那样工作:

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

作为一种解决方法,这很好,但在我看来并没有真正消除允许装饰器更改类型的优点。

编辑:它也失去了 init 部分的打字。

使用此功能,您将能够更轻松地编写 HoC
希望增加这个功能

@kgtkr这是我非常想要这个的主要原因......

react-router定义中也有一点紧急情况,因为有些人认为类型安全比拥有类装饰接口更重要。 最根本的原因是withRouter使得一些道具是可选的。

现在似乎发生了争执,人们被迫使用withRouter的函数接口而不是 decoration

开始解决此功能将使世界更快乐。

不久前, material-ui类型和withStyle也发生了同样的争执,它被设计为用作装饰器,但是为了以类型安全的方式使用,TypeScript 用户需要使用作为常规功能。 尽管尘埃落定,但对于该项目的新手来说,这仍然是一个持续困惑的根源!

*好吧,“不和”可能是一个强烈的词

我已经看了很长时间了,在它到来之前,其他人有望从我的小技巧中受益,以向前兼容的方式实现这种行为。 所以在这里,实现一个超级基本的 Scala 风格案例类copy方法......

我的装饰器是这样实现的:

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 中使用它,并且实际上让类型系统在目标类中反映新方法,可以使用以下 hack:

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

MyCaseClass的所有实例都将从CaseClass装饰器中的匿名类公开继承的copy方法的类型。 而且,当 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 示例是一个类 mixin 用例,您可以从 mixins 中获得正确的类型。 代替:

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包装器,而不是实现实际接收classes $ 属性的A类。

无论您如何格式化它,从根本上来说,extend withStyles(styles)(...) 听起来都不是一个合适的建议。 withStyles 不是一个类混合。

的确。 不知道为什么这里有这么多的阻力。

也就是说,装饰器非常接近第 3 阶段,据我所知,所以我想 TS 很快就会在这里得到适当的支持。

我听说有人想要更简洁的语法,尤其是。 当使用多个 HoC 作为装饰器时。 我们使用的临时解决方法是将多个装饰器实际包装在一个管道函数中,然后使用该纯函数来装饰组件。 例如

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

其中flowlodash.flow 。 许多实用程序库都提供了类似的功能 - recomposeRx.pipe等,但如果您不想安装库,当然可以编写自己的简单管道函数。

我发现这比不使用装饰器更容易阅读,

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

我使用它的另一个原因是,一旦宣布装饰器规范并得到适当支持,这种模式就很容易找到并替换/grep-able。

@justinfagnani奇怪的是,当我尝试将extends React.Component<Props, State>更改为extends withStyles(styles)(React.Component<Props, State>)时,我从 ESLint 收到语法错误,因此我无法以相同的方式验证 @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 类型增强的正确类型? 看起来你不会。

作为参考,我偶然发现了这一点,因为我也试图使用 Material UI 的withStyles作为装饰器,但它不起作用所以我问了这个问题: https ://stackoverflow.com/questions/53138167

需要这个,然后我们可以让异步函数返回为蓝鸟

我试图做类似的事情。 ATM 我正在​​使用以下解决方法:

首先这是我的“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 的装饰器,只需将其作为函数调用,您将获得正确的 intersection-Type 但失去在类本身内部进行类型检查的能力,因为您不能使用界面技巧了,它看起来更难看。 例如:

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 - 这里的“最终确定”是什么意思? TC39 的第 3 阶段是否符合条件?

我没有意识到他们已经走到了这一步! 他们什么时候达到第三阶段? 是最近的吗?

他们还没有; 我相信他们会在 1 月的 TC39 会议上再次从第 2 阶段推进到第 3 阶段。

您可以密切关注议程以获取详细信息。

正确(尽管我无法确认它是否会在一月份得到提升)。 我只是在问第 3 阶段他们到达那里是否有资格。

在我看来,decorator 提案还是有很多严重的问题,所以如果在 1 月份推进到 stage 3,它会再次犯错,就像有问题的 class fields 提案和 globalThis 提案一样。

@hax你能详细说明吗?
我真的很想要这个,并且发现在这件事上缺乏沟通令人难过,不幸的是没有听说过这些问题。

@AllNamesRTaken检查装饰器提案的问题列表。 😆 例如,装饰器参数之前/之后的export是阶段 3 的阻塞器。

还有一些提议的更改,例如重新设计 API,我认为这意味着该提议在第 3 阶段不稳定。

它还与有问题的类字段提案有关。 虽然class fields已经到了stage 3,但是问题太多了。 我担心的一个大问题是它给装饰者留下了很多问题(例如 protected ),这对两个提案都不利。

请注意,我不是 TC39 代表,一些 TC39 代表从不同意我对许多问题当前状态的评论。 (特别是我强烈认为目前的 TC39 流程在很多有争议的问题上都存在很大的失败。将一些问题推迟到装饰者并不能真正解决问题,只会让装饰者提案更加脆弱。)

我认为这些事情会得到解决,提案会很好,但我希望我们不在这里讨论提案的状态。

我认为有足够信心的第 3 阶段或第 4 阶段可能是我们实施新提案的地方,然后我们可以研究这个问题。

谢谢丹尼尔! 第 3 阶段通常意味着对第 4 阶段的强烈信心,因此为 1 月份的会议祈祷。

并感谢您在此处阻止装饰器讨论。 这个功能引起的愤怒和自行车脱落的程度很奇怪。 我从来没有见过这样的东西😂

只是为了记录,有一个关于同一件事的功能请求: https ://github.com/Microsoft/TypeScript/issues/8545

当您从 JavaScript 编译时,TypeScript 在一定程度上支持此功能(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-classes 来获得这种效果
https://mariusschulz.com/blog/mixin-classes-in-typescript

@Bnaya这完全不是我们现在想要的;)。
装饰器让我们避免创建我们从未打算使用的类,就像我们在做老式 Java 一样。 装饰器将允许一个非常干净整洁的组合架构,其中类可以继续执行核心功能并在其上组合额外的通用功能。 是的,mixins 可以做到,但它是创可贴的。

装饰器提案在过去几个月中发生了显着变化 - TS 团队极不可能将任何时间投入到即将不兼容的当前实施中™。

我建议观看以下资源:

装饰者提案: https ://github.com/tc39/proposal-decorators
TypeScript 路线图: https ://github.com/microsoft/TypeScript/wiki/Roadmap

@Bnaya这完全不是我们现在想要的;)。
装饰器让我们避免创建我们从未打算使用的类,就像我们在做老式 Java 一样。 装饰器将允许一个非常干净整洁的组合架构,其中类可以继续执行核心功能并在其上组合额外的通用功能。 是的,mixins 可以做到,但它是创可贴的。

它不是混合,而是一个类。
它不是坏拷贝的东西混合
当管道运算符到达时,语法也将是合理的

您可以使用 mixin-classes 来获得这种效果

在 TypeScript 中,类工厂 mixin 可能会很痛苦; 即使它们不是,它们也非常样板化,要求您将类包装在一个函数中以转换为 mixin,如果您正在导入第 3 方类,有时您不能简单地做到这一点。

以下内容更好、更简单、更简洁,并且适用于导入的 3rd 方类。 我让它在纯 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

这完全为您提供了类工厂 mixins 的功能,而无需所有样板。 不再需要围绕类的函数包装器。

但是,如您所知,装饰器无法更改已定义类的类型。 😢

因此,目前,我可以执行以下操作,唯一需要注意的是受保护的成员不可继承:

// `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 ,试图展示解决方法(这有点hacky,但很有帮助)

是否有人可以对此有所了解。 甚至考虑过内部讨论吗?

tl;dr - 这个功能在 TC39 中被卡住了。 这根本不能怪TS人。 他们不会实施,直到它成为标准。

但这是微软,他们可以实现它,然后对标准人员说——“看,人们已经在使用我们的版本了”:trollface:

这里的问题是,自从 TypeScript 实现了装饰器以来,装饰器提案已经_显着_(以向后不兼容的方式)发生了变化。 当新的装饰器最终确定并实现时,这将导致痛苦的情况,因为有些人依赖于 TypeScript 当前的装饰器行为和语义。 我强烈怀疑 TypeScript 团队希望避免处理这个问题,因为这样做会鼓励进一步使用当前的非标准装饰器,这最终会使过渡到任何新的装饰器实现更加痛苦。 从本质上讲,我个人只是没有看到这种情况发生。

顺便说一句,我最近创建了#36348,这是一个可以提供非常可靠的解决方法的功能提案。 随意看看/提供反馈/野蛮它。 🙂

tl;dr - 这个功能在 TC39 中被卡住了。 这根本不能怪TS人。 他们不会实施,直到它成为标准。

鉴于这一事实,开发团队最好写一个快速解释和状态更新,并锁定此对话,直到 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 编译器能够识别来自装饰器的返回值的类型签名会很好,但我同意应该澄清文档来代替更大的变化。

作为记录,这是我尝试使用@orta PR 中相关文档格式的简化版本:

interface HasNewProperty {
  newProperty: string;
}

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

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

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

// Alas, this line makes the compiler angry because it doesn't know
// that Greeter now implements HasNewProperty
console.log(new Greeter("world").newProperty);
此页面是否有帮助?
0 / 5 - 0 等级