I have a type guard check function that does tells JavaScript that a value is a promise or not, and at the same time it tells TypeScript that the variable is a 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'
}
It works really well, however, I realised I need to not hard code the promise resolve to any
but infer that type instead.
This one example why I saw that any
doesn't work properly:
export type PlainObject = { [key: string]: any }
let a: PlainObject | PlainObject[] | Promise<PlainObject | PlainObject[]>
let b = isPromise(a) ? await a : a
In this example, b
is inferred as PlainObject
while it SHOULD be PlainObject | PlainObject[]
....
Question 1: Why is this happening?
My attempted solution at a better isPromise
function:
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'
}
In theory I don't see why this wouldn't work. But I get this error:
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'
Question 2: Why doesn't payload is Promise<Unpacked<T>>
work and
how could I infer this otherwise?
Here is everything above in the TypeScript PlayGround:
1: Why is this happening?
PlainObject[]
is a subtype of PlainObject
. In set theory, when you have a union Subtype | Supertype
, then the resulting type will be Supertype
- it absorbs Subtype
. So b
gets type PlainObject
here.
Why is PlainObject[]
a subtype? It gets clearer, when we look at Array
and PlainObject
types:
interface Array<T> { [n: number]: T} // T is PlainObject for PlainObject[]
type PlainObject = { [key: string]: any }
In JS, number
property key will become string
. And no matter what T
is, it is assignable to 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?
For custom type guards, the type predicate (is xxx
) must be a subtype of the checked argument. TS cannot verify here, that Promise<Unpacked<T>>
is a subtype of T
. Neglecting that fact, isPromise
does its job filtering out all incompatible values of a
:
let b = isPromise(a) ? await a /*Promise<PObj | PObj[]>*/ : a /*PObj | PObj[]*/
any
- as often - is the evil one :o). A solution is to make PlainObject[]
incompatible to PlainObject
:
export type PlainObject = { [key: string]: unknown } // e.g. replace `any` by `unknown`
let b = isPromise(a) ? await a : a // b: PlainObject | PlainObject[]