这是承诺类型与其解析值不一致的代码:
const p: Promise<number> = Promise.resolve(1);
const q: Promise<string> = p.then<string>();
const r: string = await q;
// at this point, typeof r === 'number'
此时,类型系统表示 r 是一个字符串,但运行时行为是 r 是一个数字。我不认为
.then<string>
是类型断言。
这是承诺的真实运行时行为(特别是在没有提供回调的情况下)无法在打字稿类型系统中表达的情况吗?
注意:.then() 的输入来自 typescript/lib/lib.es5.d.ts:
interface PromiseLike<T> {
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
* @param onrejected The callback to execute when the Promise is rejected.
* @returns A Promise for the completion of which ever callback is executed.
*/
then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): PromiseLike<TResult1 | TResult2>;
}
您遇到了 microsoft/TypeScript#58619,它尚未被声明为错误或设计限制,但看起来这里没有太多工作要做。
then()
的Promise<T>
方法(这就是上面的p
,尽管PromiseLike<T>
非常相似)在TypeScript库中声明为与此等效:
interface Promise<T> {
then<R1 = T, R2 = never>(
onfulfilled?: ((value: T) => R1 | PromiseLike<R1>) | null,
onrejected?: ((reason: any) => R2 | PromiseLike<R2>) | null
): Promise<R1 | R2>;
}
这是一个单一的 generic 调用签名,支持有效调用,其中 可选
onfulfilled
和 onrejected
回调要么都提供,要么都省略,或者提供 onfulfilled
而拒绝 onrejected
。这些的预期用途是让调用者不必手动指定 R1
或 R2
类型参数。相反,您应该只传递参数,编译器将推断它们:
const p: Promise<number> = Promise.resolve(1);
const psb = p.then(n => "" + n, e => false);
// ^? const psb: Promise<string | boolean>
const ps = p.then(n => "" + n);
// ^? const ps: Promise<string>
const pn = p.then();
// ^? const pn: Promise<number>
在
psb
中,R1
从回调中推断为 string
,而 R2
从回调中推断为 boolean
,因此您得到 Promise<string | boolean>
。在 ps
中,R1
从回调中推断为 string
。但是R2
没有推理站点,因为省略了相关回调。因此推理失败,因此退回到never
的默认类型参数(这就是
T2 = never
的意思)。所以你得到 Promise<string | never>
这只是 Promise<string>
。对于 pn
,也没有 R1
的推理站点。因此 R1
回退到其默认类型参数 T
,即 number
。所以你得到 Promise<number | never>
这只是 Promise<number>
。
这些都是完全合理的行为,对吗?如果您调用
then()
并让 TypeScript 推断类型参数,那么一切都会正常进行。 这通常是人们实际上do称呼then()
的方式。
但是,尽管单个调用签名很方便并且可以很好地支持预期用例,但由于您发现的原因,它在技术上是错误的。默认类型参数无法阻止某人手动指定类型参数:
const q = p.then<string>(); // okay
// ^? const q: Promise<string>
几乎没有人这样做。但类似下面的东西也有同样的问题:
const qs: Promise<string> = p.then(); // okay
因为 TypeScript 使用返回类型作为
R1
的上下文类型,并且会出现相同的基本问题。
问题在于通用默认值并不优先于手动指定的类型参数。他们不是故意的。 TypeScript 缺乏一种方法来表达默认函数参数应该约束相应类型参数的情况。这或多或少是功能请求和 microsoft/TypeScript#56315 的问题,除了它们还与使用通用默认值安全地实现函数有关。 目前您能得到的最接近的方法是放弃通用默认值,只使用
overloads 来准确说明每个缺失或存在的函数参数的预期行为。像这样的东西也许适用于Promise
:
interface Promise<T> {
then(onfulfilled?: null): Promise<T>;
then<R1>(
onfulfilled: ((value: T) => R1 | PromiseLike<R1>),
onrejected?: null
): Promise<R1>;
then<R1, R2>(
onfulfilled: ((value: T) => R1 | PromiseLike<R1>),
onrejected: ((reason: any) => R2 | PromiseLike<R2>)
): Promise<R1 | R2>;
}
现在,如果您在不带任何参数的情况下调用
then()
,则
无法指定类型参数(第一个调用签名甚至不是通用的),因此您会得到
Promise<T>
。如果你用一个参数调用它,你可以指定R1
,其中必须与
onfulfilled
一致...但是你不能指定
R2
,所以你得到Promise<R1>
。 只有当你用两个参数调用它时,你才能同时指定 R1
和 R2
,然后这些 must与回调一致。因此,从呼叫者的角度来看,这些是最安全的处理方式:
const q: Promise<string> = p.then<string>(); // error
但是重载更加复杂并且有一些警告。面对重载时的推理效果很差。改变对语言如此重要的东西可能会导致现实世界中太多的破坏被接受。几乎可以肯定的是,治疗方法比疾病本身更糟糕。如果他们对 microsoft/TypeScript#58619 进行分类,我希望听到这是一个设计限制,人们应该……不要这样称呼
then()
。
Playground 代码链接