前言

写鸿蒙的时候踩了个挺恶心的坑,await 一个 Promise 方法,结果拿到的类型是 void & Window。你说离不离谱?

问题现场

代码很简单,就是想拿当前窗口实例:

const win = await window.getLastWindow(this.getUIContext().getHostContext());

写完一看,IDE 提示 win 的类型是 void & Window

什么鬼?void 是哪来的?我 await 的明明是个 Promise<Window> 啊。

鼠标悬到 getLastWindow 上一看,工具提示匹配到的是这个重载:

// 匹配到了 callback 版本
function getLastWindow(ctx: BaseContext, callback: AsyncCallback<Window>): void;

而不是我期望的 Promise 版本:

// 我想用的是这个
function getLastWindow(ctx: BaseContext): Promise<Window>;

更离谱的是,如果我手动加个 .then()

const win = await window.getLastWindow(ctx).then();

A clean, structured Notion-style technical diagram

类型就正常了,win 变成了 Window

这到底是怎么回事?


根因:TypeScript 的重载解析规则

要理解这个问题,得先知道 TypeScript 处理函数重载的机制。

window.getLastWindow@ohos.window 的声明文件里有两个重载签名:

// 重载1:callback 版本(排在前面)
function getLastWindow(ctx: BaseContext, callback: AsyncCallback<Window>): void;

// 重载2:Promise 版本(排在后面)
function getLastWindow(ctx: BaseContext): Promise<Window>;

TypeScript 的重载解析规则是从上往下匹配,命中第一个就停

当你只传一个 ctx 参数时,两个重载都能匹配。callback 那个虽然第二个参数没传,但 TS 对可选参数的判定比较宽松,它觉得"嗯,这个也说得通"。于是直接命中了排在上面的 callback 版本,返回值就是 void

然后你对一个 void 执行 await,TS 把 voidawait 解包后的类型做了交叉,最终就得到了 void & Window 这个缝合怪。


为什么加了 .then() 就好了?

const win = await window.getLastWindow(ctx).then();

加了 .then() 之后,TS 看到了一个关键信号:你在返回值上调了 .then()

.then()Promise 独有的方法,void 上没有这个东西。这就迫使 TS 重新审视重载列表,发现只有第二个重载(返回 Promise<Window> 的那个)才能让 .then() 合法调用。于是匹配到了 Promise 版本,类型推导一切正常。

说白了,.then() 在这里起到了类型歧义消除器的作用。你不是在用它处理数据,你是在帮编译器做选择题。

A three-column comparison table styled like a Noti


3 种更干净的解法

虽然 .then() 能 work,但每次写都觉得别扭。下面给几种更优雅的方案。

解法一:显式标注返回类型(推荐)

最直接的方式,给变量标上你想要的类型:

const win: window.Window = await window.getLastWindow(
  this.getUIContext().getHostContext()
);

TS 会根据你标注的目标类型做反向推导,自动选到 Promise 重载。代码干净,意图明确。

我个人最推荐这种写法,因为读代码的人一眼就知道你要拿的是什么。

解法二:类型断言

as 显式告诉编译器返回值的类型:

const win = await (window.getLastWindow(
  this.getUIContext().getHostContext()
) as Promise<window.Window>);

效果和解法一差不多,区别在于断言写在右边,类型标注写在左边。看个人习惯。

解法三:用 Promise 变量接一下

const windowPromise: Promise<window.Window> = window.getLastWindow(ctx);
const win = await windowPromise;

多写了一行,但语义非常清晰。适合在复杂逻辑里需要把 Promise 传递来传递去的场景。

A vertical flowchart detailing three clean solutio


不只是 getLastWindow

这个坑不是 getLastWindow 专属的。HarmonyOS SDK 里大量异步 API 都有同样的问题。

早期鸿蒙 API 的设计思路是 callback 优先,Promise 是后加的"便利版本"。所以在声明文件里,callback 重载总是排在前面。这包括但不限于:

  • window.getLastWindow
  • window.getWindow
  • http.createHttp().request()
  • 各种 xxxManager 类的异步方法

如果你在使用某个系统 API 时发现 await 出来的类型不对,八成都是同一个原因。回头看看声明文件里的重载顺序,然后用上面的解法处理就行。


写在最后

这类问题的本质是 TypeScript 重载机制和鸿蒙 API 设计风格的一次碰撞。TS 选了"先到先得"的策略,鸿蒙选了"callback 优先"的声明顺序,两边都没错,但合在一起就让开发者遭了殃。

好消息是后续版本的 SDK 已经在调整重载顺序了,Promise 版本会逐渐排到前面。如果你升级了 API 版本发现这个问题消失了,那就是华为在背后默默修了。

不过在那之前,记住一句话:await 鸿蒙 API 的时候,类型不对就显式标一下,别跟编译器赌默契。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐