如何在JavaScript Flow接口中创建封装/存在类型以隐藏内部类型变量?
这确实是Flow里处理不透明泛型类型时的典型痛点——你想让使用者完全不用关心cursor的具体类型,只需要通过接口方法操作它,就像Haskell里的存在类型那样。咱们先理清楚问题,再看两种可行的解决办法:
问题回顾
你定义的接口是这样的,cursor类型仅对接口内部方法可见,实现类决定具体类型:
interface OrderIndexI<cursor> { getCursor(): cursor; fromCursor(cursor): number; } class OrderIndex1 implements OrderIndexI<Cursor1> { ... } class OrderIndex2 implements OrderIndexI<Cursor2> { ... }
但写泛型函数时,OrderIndexI<*>无法工作,只能用OrderIndexI<any>,但这会丢失类型安全,毕竟cursor原本是封闭在接口内部的。
方案1:用包装器模拟存在类型
这是最接近Haskell存在类型的方式——创建一个包装类型,把cursor类型完全隐藏,只暴露接口的方法:
// 原接口保持不变 interface OrderIndexI<cursor> { getCursor(): cursor; fromCursor(cursor): number; } // 定义一个“任意OrderIndex”类型,隐藏cursor type AnyOrderIndex = { getCursor(): mixed; fromCursor(cursor: mixed): number; }; // 辅助函数:把任意OrderIndexI实例转换成包装类型 function wrapOrderIndex<Cursor>(o: OrderIndexI<Cursor>): AnyOrderIndex { return { getCursor: () => o.getCursor(), fromCursor: (cursor) => o.fromCursor(cursor), }; } // 现在泛型函数可以安全接受AnyOrderIndex function genericWork(o: AnyOrderIndex) { const cursor = o.getCursor(); const num = o.fromCursor(cursor); // 虽然cursor是mixed,但只能传给同一个实例的fromCursor,完全安全 }
使用的时候只需要把实例包装一下:
const idx1 = new OrderIndex1(); genericWork(wrapOrderIndex(idx1));
这个方案的核心是,包装器内部保留了原实例的类型关联,外部只看到mixed,但无法把其他实例的cursor传给当前实例的fromCursor,保证了类型安全。
方案2:利用Flow的类型推导写泛型函数
其实你不需要显式写<*>或者<any>,Flow的类型推导可以自动识别cursor类型,只需要把函数写成带泛型参数的形式:
function genericWork<Cursor>(o: OrderIndexI<Cursor>) { const cursor = o.getCursor(); const num = o.fromCursor(cursor); // Flow会自动推导Cursor的具体类型,使用者完全不用关心 }
调用的时候直接传实例就行,Flow会自动处理:
genericWork(new OrderIndex1()); genericWork(new OrderIndex2());
这个方案更简洁,也更符合Flow的惯用写法——你不需要隐藏cursor类型,而是让Flow帮你跟踪它,同时使用者完全不需要显式指定类型参数,达到了“不用关心cursor类型”的效果。
为什么OrderIndexI<*>不行?
Flow里的* wildcard不像Java的?那样支持协变/逆变的自动处理,在函数参数位置使用OrderIndexI<*>会导致类型不匹配——Flow无法确定fromCursor接受的类型和getCursor返回的类型是同一个,所以会报错。而any虽然能绕过检查,但会丢失类型安全,这也是你觉得不合理的原因。
总结
- 如果想完全隐藏
cursor类型(严格模拟存在类型),用包装器方案; - 如果只是想写一个能处理任意
OrderIndexI的函数,用带泛型参数的函数方案,利用Flow的类型推导,既安全又简洁。
内容的提问来源于stack exchange,提问作者CMCDragonkai

