Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

empty attributes on ts models with target equal or above ES2022 #377

Open
cyjake opened this issue Mar 16, 2023 · 5 comments
Open

empty attributes on ts models with target equal or above ES2022 #377

cyjake opened this issue Mar 16, 2023 · 5 comments

Comments

@cyjake
Copy link
Owner

cyjake commented Mar 16, 2023

class Template extends Bone {
  @Column()
  id: number;

  @Column()
  createdAt: Date;
}

the model above might be instantiated like an empty object when compiled with tsconfig that has target set to ES2022 or above:

Collection (30) [
  Template {}, ...
]

this is because public class fields will be initialized with [[Define]] rather than [[Set]] when the target is ES2022 or above. ts projects with this config will have to change the https://www.typescriptlang.org/tsconfig#useDefineForClassFields property to false.

@cyjake cyjake pinned this issue May 10, 2023
@JimmyDaddy
Copy link
Collaborator

Or use declare to define a column

class Base extends Bone {
  @Column()
  declare id: bigint;
}

class Note extends Base {
  @Column()
  declare body: string;
}

@coinkits coinkits unpinned this issue Aug 26, 2024
@cyjake cyjake pinned this issue Feb 10, 2025
@cyjake
Copy link
Owner Author

cyjake commented Feb 11, 2025

@JimmyDaddy 又研究了一下修复方法,不考虑去掉 class fields 或者保持关闭 useDefineForClassFields 的话,用你提到的这个 declare class field 是最简单的方式

就是对 user land 入侵有点严重,model fields 得改一遍

@cyjake
Copy link
Owner Author

cyjake commented Feb 11, 2025

重新记录一下 bug 成因,es2022 之后 class fields 的初始化行为从 set 变成了 define,也就是原本是:

class Bar {
  constructor(values) {
    for (const name in values) this[name] = values[name];
  }
}

现在变成了:

class Bar {
  constructor(values) {
    Object.defineProperty(this, 'a', { value: values[name], ... });
  }
}

并且如果类之间有继承关系,情况会更加复杂一些,例如下面这个写法:

class Bar extends Foo {
    c = 2;
}

编译后是:

class Bar extends Foo {
    constructor() {
        super(...arguments);
        Object.defineProperty(this, "c", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: 2
        });
    }
}

在 Leoric 里面,只能在基类 Bone 里面初始化这些值,也就是上面这个示例里面的 super(),此时子类还没有覆盖属性定义。而且提前阻止子类里的 Object.defineProperty() 也不行,会报 redefine property 错误。

@cyjake
Copy link
Owner Author

cyjake commented Feb 11, 2025

动态覆盖子类的 constructor 去恢复 attribute getter/setter 也不可行

@cyjake
Copy link
Owner Author

cyjake commented Feb 11, 2025

总结一下目前升级 target >= ES2022 可选的解决办法:

  • 手动关闭 useDefineForClassFields
  • 手动替换所有的 model class fields,加上 declare 前缀(加上这个前缀后,typesccript 转换后的代码不再包含 class fields,而是用最原始的在 constructor() 调用 this[filed] = ... 的方式 https://www.typescriptlang.org/docs/handbook/2/classes.html#type-only-field-declarations
  • 仍然切换到 [[DefineOwnProperty]],代价是禁止 new Model(),改成统一用 Model.init() 之类的方式初始化(Model.create() 方法现在就有,只是后者会自动执行 insert,插入成功后返回 model 实例

还有一个解法需要在框架层处理,就是在 egg-orm、@midwayjs/leoric 之类的组件里面,在 Model 加载阶段,主动 subclass 一下,在 subclass 的 constructor 里面恢复 attributes 对应的 property descriptor,代价是 cnpmcore 里这种直接 import 的就会 break

手动关闭 useDefineForClassFields 也有些不太可取,毕竟这个是 ES2022 之后 js 运行时的默认行为,即便 ts 可以通过编译解决,裸写 js 的项目也还是会遇到问题

增加 declare 前缀的 cons 也是同样如此

怎么选呢

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants