在打字稿中返回`payload is T`时如何推断正确的类型?

How to infer correct type when returning `payload is T` in typescript?

我有一个类型保护检查函数,它告诉 JavaScript 一个值是否是一个 promise,同时它告诉 TypeScript 该变量是一个 promise:

function getType (payload: any): string {
  return Object.prototype.toString.call(payload).slice(8, -1)
}    

function isPromise (payload: any): payload is Promise<any> {
  return getType(payload) === 'Promise'
}

它工作得很好,但是,我意识到我不需要将承诺解析硬编码为 any,而是推断该类型。

这是我看到 any 无法正常工作的一个例子:

export type PlainObject = { [key: string]: any }
let a: PlainObject | PlainObject[] | Promise<PlainObject | PlainObject[]>
let b = isPromise(a) ? await a : a

在这个例子中,b 被推断为 PlainObject 而它应该是 PlainObject | PlainObject[]...

问题 1:为什么会这样?

我在更好的 isPromise 函数上尝试的解决方案:

type Unpacked<T> =
    T extends (infer U)[] ? U :
    T extends (...args: any[]) => infer U ? U :
    T extends Promise<infer U> ? U :
    T;

function isPromise2 <T extends any>(payload: T): payload is Promise<Unpacked<T>> {
  return getType(payload) === 'Promise'
}

理论上我不明白为什么这行不通。但是我得到这个错误:

A type predicate's type must be assignable to its parameter's type. Type 'Promise>' is not assignable to type 'T'. 'Promise>' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'any'

问题 2: 为什么 payload is Promise<Unpacked<T>> 不起作用并且 否则我怎么能推断出这个?

以上是 TypeScript PlayGround 中的所有内容:

https://www.typescriptlang.org/v2/en/play?#code/KYDwDg9gTgLgBDAnmYcAKAbAhgSwHYDyARgFbADG8AvHAN5wDaA1sIgFxwDOMU+A5gF0OWPIjgBfAFCSAZgFc8lHBDxw+wGABVkqABRgsiDBCwATYaICUHbrzx86kuHCga5UVcTKUAdGCgQMIE6PkEAyjz8PuRYGBj6hsZmlj6cGDjkwLoAHAA0cAC0AIyWkuLOztLyijDKqjicaAEAtg16BkYm5nAiiNZwHUmmcA3oLW0APL0AfI7OrjDuqupaOgmdyXBU23AA5E0QrZzAu2XSGBo9HJi4hKQU8AA+6Nj4Xg8MAnDPB0fAEzc3vdKN8Xrd3pRPtNJBd4EQtiNGuNjrosJY4AB+HoAd1w8CwcGE0gA9MS4NiABZYeAASRcwFiGDEuLw8CCcFMEERbEkSBQcAAqngDOQWKYJppZlQnBVNHBQDBgHhTJw4Lp8DJgFBBZZPpjBYSZc45QqlSq1T5LVgoHxOBZEJ90VRZhqtQasQLDRU4CaQIrlarfpNXdqBbMPV7ZQBuKoKJQqRFB44AJjgEvlfrNqpm6yGHE0-UGXURY0OkyFIrFEums1oMoWSzUGm0KFzXSdO32yJOZyAA

1: Why is this happening?

PlainObject[]PlainObject 子类型 。在集合论中,当你有联合 Subtype | Supertype 时,结果类型将是 Supertype - 它 absorbs Subtype。所以 b 在此处获取类型 PlainObject

为什么 PlainObject[] 是子类型?当我们查看 ArrayPlainObject 类型时,它会变得更清楚:

interface Array<T> { [n: number]: T} // T is PlainObject for PlainObject[]
type PlainObject = { [key: string]: any }

在JS中,number 属性键会变成string。而且无论 T 是什么,它都可以分配给 any.

type PlainObjArr_extends_PlainObj  = PlainObject[] extends PlainObject ? true : false // true

2: Why doesn't payload is Promise> work and how could I infer this otherwise?

对于自定义类型保护,type predicate (is xxx) 必须是已检查参数的 子类型 。 TS 无法在这里验证 Promise<Unpacked<T>>T 的子类型。忽略这个事实,isPromise 确实 它的工作是过滤掉 a:

的所有不兼容值
let b = isPromise(a) ? await a /*Promise<PObj | PObj[]>*/ : a /*PObj | PObj[]*/

any - 通常 - 是邪恶的 :o)。 solution 是使 PlainObject[]PlainObject 不兼容:

export type PlainObject = { [key: string]: unknown } // e.g. replace `any` by `unknown`
let b = isPromise(a) ? await a : a  // b: PlainObject | PlainObject[]