我试图在后端节点项目中将FlowJS与Sequelize一起使用。但是,没有关于如何注释所涉及的数据类型的特定FlowJS文档,并且我得到的代码可以工作但也会引发很多类型错误。
我有一个完整的演示项目on github的工作版本。
我们从基本的模型定义开始。我最近一直在玩很多Stellaris,所以我在想这里的盛大银河策略。
// @flow
import Sequelize, { type Model } from "sequelize";
export type Empire = {
name: string,
species: string,
homeworld: string
};
const EmpireModel = (sequelize: Sequelize): Class<Model<Empire>> =>
sequelize.define("empires", {
name: { type: Sequelize.STRING, allowNull: false },
species: { type: Sequelize.STRING, allowNull: false },
homeworld: { type: Sequelize.STRING, allowNull: false }
});
module.exports = EmpireModel;
根据我的想法,我想要一个包含所有Sequelize函数表的数据类型操作函数(create,destroy,findAll等),我想我想要另一种数据类型,它只代表所涉及的实际对象(一个特定的帝国存储在数据库)。我可能完全错了,并愿意接受如何使上述更好的建议。
上面的代码类型检查很好,但我不确定Class<Model<Empire>>
是来自sequelize.define
的返回值的正确注释。
现在我们来看看应用程序本身:
// @flow
import Sequelize, { type Model } from "sequelize";
import { type Empire } from "./empire";
const sequelize = new Sequelize(
"postgres://postgres:sequelize@localhost:5432/sqldemo"
);
const main = async () => {
const EmpireModel: Class<Model<Empire>> = await require("./empire")(
sequelize
);
const empire: Model<Empire> = await EmpireModel.create({
name: "Sildaran Republic",
species: "Sildar",
homeworld: "Sakatarola"
});
const empires = await EmpireModel.findAll();
console.log(empires);
empires.map(empire => {
console.log("[Empire] ", empire.name);
console.log("[Founding Species] ", empire.species);
console.log("[Homeworld] ", empire.homeworld);
});
};
这段代码实际上运行正常,但是typechecker在行await Empires.findAll()
上标记了一个错误。错误消息是
Cannot call await with `EmpireModel.findAll()` bound to `p` because:
- Either cannot get `empire.name` because property `name` is missing in `Model` [1].
- Or cannot call `empires.map` because property `map` is missing in `Promise` [2].
src/index.js:20:25
20| const empires = await EmpireModel.findAll();
^^^^^^^^^^^^^^^^^^^^^
References:
flow-typed/npm/sequelize_v4.x.x.js:3264:72
3264| options?: FindOptions<TAttributes & TCustomAttributes>): Promise<this[]>,
^^^^ [1]
flow-typed/npm/sequelize_v4.x.x.js:3264:64
3264| options?: FindOptions<TAttributes & TCustomAttributes>): Promise<this[]>,
^^^^^^^^^^^^^^^ [2]
所以,我实际上有几个具体的问题:
EmpireModel
中分配给empires.js
的实际正确类型是什么?await require("./empire")(sequelize);
的返回值的实际类型是什么,以及当我弄错时如何让Flow中断(Flow对我指定的任何类型都感到高兴,并且当我以与该类型不一致的方式使用该值时仅标记错误)我也对所有其他建议持开放态度,以使此代码更正确。
简短的回答:
对于某些模型x
,您必须调用x.get()
或x.toJSON()
1来获取模型底层的普通对象。如果您在查询后更改代码
empires.map(empire => empire.get())
.map(empire => {
console.log("[Empire] ", empire.name);
console.log("[Founding Species] ", empire.species);
console.log("[Homeworld] ", empire.homeworld);
});
然后它会正常工作。
答案很长:
这个解决方案看起来有点武断,所以这是我过去常常找到它的过程。不幸的是,Flow的好处是以学习其局限性以及如何调试其错误消息为代价的,所以希望这会对我如何做到这一点拉开帷幕。
我经常检查的第一件事是所有类型都正确导入。 flow-typed
目录在错误消息中,Empire
类型也是如此,所以这看起来不错。
接下来,错误消息为我们提供了两种可能性,我想弄清楚我们正在处理哪一种。 empires.map
无效,或empire.name
无效。所以我注释掉了map
中的现有代码,并将其替换为身份函数empire => empire
。当我这样做时,错误就消失了,所以很明显问题是访问empire.name
。
为了弄清楚为什么这是一个问题,我为Sequelize挖了一个libdef。它位于流式存储库here中。 Libdefs可能看起来令人生畏,但它们只是添加了declare
关键字的正常流定义。在正常的代码中你可能会说let i : number = 1
,你会说declare
let i = 1;
declare i : number
这些之间的区别在于流量类型检查正常注释以确保它们有意义,但它将declare
语句视为gospel2。考虑到这一点,我们可以阅读libdef本身,看看我们正在处理的类型和接口。因为libdefs使用declare
,Sequelize包中的实际代码与类型检查无关; Flow只查看libdef,而不是实现。
这里的评论中有相当详尽的文档,但遗憾的是文件长度超过7k,并没有将最重要的定义放在首位。
Model
类的声明生活here,findAll
的定义生活here。我们可以看到findAll
返回一个this
类型的数组,意思是一个类型为Model
的数组,其类型参数与我们当前的Model
相同。回到Model
的顶部,我们看到以下参数:
Model<TAttributes, TInitAttributes = TAttributes, TPlainAttributes = TAttributes>
虽然Model
类型有3个参数,但其中两个是可选的,默认情况下设置为第一个参数。
所以我们的问题是我们得到一个类型为Model<Empire, Empire, Empire>
的对象数组,我们正在映射它,然后我们尝试访问Empire
类型中定义的字段。在非类型化的JavaScript中这很好,因为库可以动态地将字段添加到模型中,但是在Flow中,将一个对象类型“合并”到另一个对象类型需要使用交集类型,并且可以有一些粗略的边缘情况。幸运的是,在文件后面查看sequelize.define
的定义表明,这里没有做过这样的奇特事情。如果我们想要访问包含的Empire
,我们需要找到一个方法,其返回值是Model
的泛型类型参数之一。
我们可以看到那些here,最后一个定义包含所有可选参数,在简单的答案中为我们提供了解决方案。搜索课程的其余部分也会出现toJSON function.
1请注意,toJSON
返回一个对象,而不是JSON字符串。当一个对象被传递到JSON.stringify
时,stringify
将调用该对象上的toJSON
函数(如果它存在)并将字符串化toJSON
返回的对象。这就是为什么你可以在console.log
对象上调用Model
并查看你在这个模型上定义的属性,而不是Sequelize附加到模型的所有其他字段。这个名称具有误导性,但这是JavaScript的错,而不是Sequelize的错。
2技术上有一些检查。例如,如果你declare
变量同时具有类型string
和number
,你将得到一个错误。仅检查declare
注释与其他declare
注释的一致性,然后检查非declare
注释,确保如果有错误,则报告为正常注释的问题,而不是declare
注释。
免责声明:我真的不知道任何Flow,并且我使用TypeScript和Haskell背景来实现这一点,而且不幸的是,TypeScript并没有真正有任何亮点(.define()
的输出只是一个Model<Empire, Empire>
而且它有一个适当的.findAll()
方法) - 但请不要认为我比这更专业。
如果我查看test suite的流程类型,他们并不总是使用Class<>
utility type键入s.define()
的输出,但偶尔使用typeof
,这似乎对应于使用子类的愿望:
type WarehouseAttributes = {
id?: number;
address?: string;
capacity?: number;
};
declare class WarehouseInstance extends Model<WarehouseAttributes> {
id?: number;
address?: string;
capacity?: number;
// ... mixins ...
};
// ... then this gets used to define ...
let Warehouse: typeof WarehouseInstance = s.define('warehouse', {});
// ... then this gets used to query ...
Warehouse
.findAll({include: [{association: WarehouseProducts}]})]
.then((warehouses: Array<WarehouseInstance>) => {})
是否有可能这样的方法允许你返回一个声明的类而不是像你现在正在做的那样的Class<Model<EmpireAttributes>>
?也许有必要让Promise<this[]>
有一个this[]
,其中包含Empire
属性?