多种类型的 TC39 装饰器的正确键入

问题描述 投票:0回答:1

我正在尝试编写一个日志记录装饰器工厂,我可以在类、方法和箭头函数字段中重用它。我正在使用 TS 5.6,不确定它们是否包含更新的类型。我已经使用关于装饰器的TS博客文章2ality的参考文章让它在箭头函数字段中工作,但是当我将其扩展为也支持类和方法装饰器时,我似乎无法让它工作基于“种类”的条件:

type Constructor<T = object> = new (...args: unknown[]) => T;

// factory function that returns a decorator
export function log<This, Value, Args extends unknown[], Return>(
  prefix = '[LOG]'
) {
  // return a decorator function
  return function (value: Value | undefined, context: ClassFieldDecoratorContext | ClassMethodDecoratorContext | ClassDecoratorContext) {
    const name = String(context.name);

    // class and methods, value is a constructor or a function
    if (context.kind === 'class') {
      const OriginalClass = value as Constructor;
      return class extends OriginalClass {
        constructor(...args: unknown[]) {
          console.log(`${prefix} >> ${name}`);
          console.log(`${prefix} -- ${name} Args: ${JSON.stringify(args)}`);
          super(...args);
          console.log(`${prefix} << ${name}`);
        }
      }
    }

    if (context.kind === 'method') {
      // with methods you can replace the original function
      return function (this: This, ...args: Args): Return {
        console.log(`${prefix} >> ${name}`);
        console.log(`${prefix} -- ${name} Args: ${JSON.stringify(args)}`);
        const result = (value as (...args: unknown[]) => unknown).apply(this, args) as Return;
        console.log(`${prefix} << ${name} = ${JSON.stringify(result)}`);
        return result;
      };
    }

    // class fields, value is undefined
    if (context.kind === 'field') {
      // with class fields you can only replace the initial value
      return function (this: This, initialValue: Value) {
        if (typeof initialValue === 'function') {
          // if the value is an arrow function, return a new function
          return function (this: This, ...args: Args): Return {
            console.log(`${prefix} >> ${name}`);
            console.log(`${prefix} -- ${name} Args: ${JSON.stringify(args)}`);
            const result = initialValue.apply(this, args) as Return;
            console.log(`${prefix} << ${name} = ${JSON.stringify(result)}`);
            return result;
          };
        }

        // fallback to the initial value
        return initialValue;
      };
    }

    // fallback to the original value
    return value;
  };
}

@log('[CLASS]')
class Hello {
  #name: string;

  constructor(private name?: string) {
    this.#name = name ?? 'World!';
  }

  @log('[METHOD]')
  method() {
    return `Hello, ${this.#name}!`;
  }

  @log('[AFF]')
  aff = () => `Hello, ${this.#name}!`;
}

// check for correct value and binding
const hello = new Hello('Name!');
const aff = hello.aff;

console.log(hello.method());
console.log(hello.aff());
console.log(aff());

它似乎表现正确,但在我的 IDE 中弹出的 TS 错误是沿着这些线的,我不知道如何修复:

Decorator function return type 'typeof Hello | typeof (Anonymous class) | ((this: unknown, ...args: unknown[]) => unknown) | ((this: unknown, initialValue: typeof Hello) => typeof Hello | ((this: unknown, ...args: unknown[]) => unknown)) | undefined' is not assignable to type 'void | typeof Hello'.
  Type 'typeof (Anonymous class)' is not assignable to type 'void | typeof Hello'.
    Types of construct signatures are incompatible.
      Type 'new (...args: unknown[]) => log<unknown, typeof Hello, unknown[], unknown>.(Anonymous class)' is not assignable to type 'new (name?: string | undefined) => Hello'.
        Type 'log<unknown, typeof Hello, unknown[], unknown>.(Anonymous class)' is missing the following properties from type 'Hello': #name, method, aff. ts(1270)
typescript
1个回答
0
投票

函数中的大多数generic类型参数根本没有任何好处。 TS 对 ES Decorators 的支持仍然不支持任何类型的 mutation 装饰事物的类型(因此 microsoft/TypeScript#4881 仍然开放),因此由

 返回的函数输出的类型log()
实际上只能是其第一个参数或
void
的类型。 由于函数的实现使用 类型断言 来表示函数属于与泛型类型参数相关的类型,并且由于这些类型参数是在
log()
上声明的,而不是在它返回的函数上声明的,因此实际上它们是什么都不做。

唯一重要的泛型类型参数是

Value
。您希望
log()
返回的函数接受
Value
类型的输入并返回
Value
类型的输出。所有其他类型都取决于您,但这是重要的一点:

function log(prefix = '[LOG]') {
  return function <Value>(
    value: Value, 
    context: ClassFieldDecoratorContext | ClassMethodDecoratorContext | ClassDecoratorContext
  ): Value {
     // impl
  }
}

请注意,我将泛型类型参数的范围从

log
移至返回的函数。因此,您不会要求 TypeScript 使用 contextualtyping 来推断
Value
。 如果你写的话,你现在的方式就可以正常工作

@log('[CLASS]')
class Hello { 
}

但如果你写表面上等价的就会失败

const logClass = log('[CLASS]');
@logClass
class Hello {
}

如果将

Value
移至返回的函数,则上面的
logClass
仍将是通用的,并且当您使用
Value
装饰器时将推断
@logClass
,而不是因
Value
unknown
而失败。


这就是

log()
在呼叫者看来应该是这样的。 至于实现,您可能需要在 return 语句上使用类型断言,以便 TS 接受它的类型
Value
,但同样,您已经在使用它们,因此它并没有真正失去任何类型安全性:

function log(prefix = '[LOG]') {
  // return a decorator function
  return function <Value>(
    value: Value,
    context: ClassFieldDecoratorContext | ClassMethodDecoratorContext | ClassDecoratorContext
  ): Value {

    if (context.kind === 'class') {
      const OriginalClass = value as Constructor;
      return class extends OriginalClass { ⋯ } as Value
    }

    if (context.kind === 'method') {
      return function (this: any, ...args: any) { ⋯ } as Value;
    }

    if (context.kind === 'field') {
      return function (this: any, initialValue: Value) { ⋯ } as Value;
    }

    return value;
  };
}

同样,您可以使用

any
Function
以外的类型,或任何您需要的类型。您可能会在某个时候决定使用更强的类型,但是装饰器不需要从调用站点工作。

Playground 代码链接

© www.soinside.com 2019 - 2024. All rights reserved.