V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
DreamingCTW
V2EX  ›  TypeScript

关于 TypeScript 装饰器和接口的问题,请教一下各位

  •  
  •   DreamingCTW · 23 小时 18 分钟前 · 434 次点击

    我是做 Java 后端的,这两天正在系统化的学习 ts ,学到装饰器的时候,有点疑惑,希望可以指导下,谢谢大家。以下是 Demo

    type Constructor = new (...args: any[]) => {};
    
    function LogTime<T extends Constructor>(target: T) {
      return class extends target {
        createTime: Date;
        constructor(...args: any[]) {
          super(...args);
          this.createTime = new Date();
        }
        getCreateTime() {
          return `该对象的创建时间是:${this.createTime}`;
        }
      };
    }
    
    @LogTime
    class Person {
      name: string;
      age: number;
      constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
      }
    }
    
    interface Person {
      getCreateTime(): void;
    }
    
    const person = new Person("小明", 18);
    console.log(person.getCreateTime());
    

    如果不声明 interface Person { getCreateTime(): void },那么 person.getCreateTime()会报错,提示 Person 对象中没有这个属性。加上这个接口就不会报错了,但有一个问题,接口中规范了 getCreateTime()函数的返回值为 void ,实际上装饰器中 getCreateTime()返回的是 string ,这不矛盾吗?我尝试修改接口中的 getCreateTime()函数的返回值,发现改成任意类型都可以...

    现在给我的感觉就是,为了让 person.getCreateTime()代码不报错,就打了 interface Person 这个补丁,而且可以不强制遵循这个补丁的规范。

    由此我想到了另一个问题,从上述代码看,Person 既是一个 class ,也是一个 interface ,我还可以创建一个新的 class 来 implements Person ,发现 class Person 中的所有属性也都要实现,所以 interface Person 是把 class Person 转成了接口?

    class NewPerson implements Person {
      name: string;
      age: number;
      createTime: Date;
      constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
        this.createTime = new Date();
      }
      getCreateTime() {
        return `该对象的创建时间是:${this.createTime}`;
      }
    }
    
    9 条回复    2025-04-03 16:49:19 +08:00
    X0V0X
        1
    X0V0X  
       23 小时 12 分钟前
    这就是人们说的 java 味吗
    FrankFang128
        2
    FrankFang128  
       23 小时 6 分钟前
    大部分 JSer 不用装饰器
    jackge0323
        3
    jackge0323  
       22 小时 35 分钟前
    这部分可以跳过,基本不用。
    sjhhjx0122
        4
    sjhhjx0122  
       22 小时 32 分钟前
    这种情况还是继承比较好,当然类装饰器也可以规定类长什么样子,这样就可以要求强制继承一个带这个方法的类了
    lisongeee
        5
    lisongeee  
       22 小时 15 分钟前   ❤️ 1
    ts 认为装饰器不会改变类型,因此你声明的时候类型是啥它就是啥

    达到你想要的效果也很简单,将声明改为调用

    const Person = LogTime(
    class {
    name: string;
    age: number;
    constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
    }
    },
    );
    lanten
        6
    lanten  
       20 小时 42 分钟前
    目前再 TS 中的任何装饰器(类/方法/属性/参数装饰器)都无法更改调用该装饰器目标的原始类型.

    由于你的 getCreateTime 方法是在装饰器中添加的, 且在原始类(Person)中没有该方法的签名, 所以 person 实例在类型系统中压根没有 getCreateTime 方法, 报错符合预期.

    声明 `interface Person` 是不完美的, 这会破坏类型系统的准确性. (但也不是不行)

    我个人认为你这种场景应该使用基类派生(extends), 而不是装饰器, 通常类装饰器需要配合方法装饰器或属性装饰器才能体现其价值
    DreamingCTW
        7
    DreamingCTW  
    OP
       19 小时 59 分钟前
    @lanten #6 我是在哔站看的 ts 视频,里面讲装饰器的时候举的这个例子,当他为了解决 person 中没有 getCreateTime 函数时,就用了 interface ,他用的时候我没觉得有问题,但当他写 getCreateTime 时,声明了 void ,我就发现了这个问题。他假设的场景是,为每一个 class 的实例对象创建时都记录一个创建时间,所以用了装饰器,这样可以很方便的为每一个 class 上写 @LogTime
    DreamingCTW
        8
    DreamingCTW  
    OP
       19 小时 58 分钟前
    @sjhhjx0122 #4 他假设的场景是,为每一个 class 的实例对象创建时都记录一个创建时间,所以用了装饰器,这样可以很方便的为每一个 class 上写 @LogTime ,只是我在学习的过程中发现了有这个问题。
    sillydaddy
        9
    sillydaddy  
       17 小时 35 分钟前
    可以给装饰器定义明确的返回类型:

    ```
    interface WithCreateTime {
    getCreateTime(): string;
    }

    function LogTime<T extends Constructor>(Base: T): T & Constructor<WithCreateTime>;
    ```

    上面的 LogTime 函数定义中,明确指定了返回类型:T & Constructor<WithCreateTime> 。并且与 LogTime 实际返回值的类型是一致的。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2338 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 02:24 · PVG 10:24 · LAX 19:24 · JFK 22:24
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.