Search code examples
typescriptpromisetype-safety

Typescript typings for failure `reason` in various Promises implementations?


The current d.ts definition files for various promise libraries seem to give up on the data type supplied to the failure callbacks.

when.d.ts:

interface Deferred<T> {
    notify(update: any): void;
    promise: Promise<T>;
    reject(reason: any): void;
    resolve(value?: T): void;
    resolve(value?: Promise<T>): void;
}

q.d.ts:

interface Deferred<T> {
    promise: Promise<T>;
    resolve(value: T): void;
    reject(reason: any): void;
    notify(value: any): void;
    makeNodeResolver(): (reason: any, value: T) => void;
}

jquery.d.ts (promise-ish):

fail(failCallback1?: JQueryPromiseCallback<any>|JQueryPromiseCallback<any>[], ...failCallbacksN: Array<JQueryPromiseCallback<any>|JQueryPromiseCallback<any>[]>): JQueryPromise<T>;

I don't see anything in the Promises/A+ spec suggesting to me that reason cannot be typed.

I did attempt it on q.d.ts but the type information seems to get lost where the transition from 'T to 'U happens and I don't fully understand why that has to be the case - and my attempts (mechanically adding 'N and 'F type parameters to <T> and 'O and 'G type parameters to <U> and typing things as I figured they ought to be) result mostly in {} being the type for the newly added type parameters.

Is there a reason that they cannot be given their own type parameter? Is there a construction of promises that can be fully typed?


Solution

  • I think one of the main challenges to achieving something like this is that the parameters to then are optional and its return type is dependent on whether they are functions or not. Q.d.ts doesn't get it right, even with just one type parameter:

       then<U>(onFulfill?: (value: T)  => U | IPromise<U>, 
               onReject?: (error: any) => U | IPromise<U>, 
               onProgress?: Function):                      Promise<U>;
    

    This says that the return type of Promise<T>.then() is Promise<U>, but if onFulfill is unspecified, the return type is actually Promise<T>!

    And this doesn't even get into the fact that onFulfill and onReject can both throw, giving you five different sources of errors to reconcile in order to determine the type of p.then(onFulfill, onReject):

    • The rejection value of p if onReject is unspecified
    • An error thrown in onFulfill
    • The rejection value of a promise returned by onFulfill
    • An error thrown in onReject
    • The rejection value of a promise returned by onReject

    I'm pretty sure there isn't even a way to express bullets 2 and 4 in TypeScript since it doesn't have checked exceptions.

    If we take the analogy with synchronous code, the resulting value of a block of code can be well defined. The possible set of errors thrown by a block of code rarely are (unless, as Benjamin points out, you are writing in Java).

    To take that analogy further, even with TypeScript's strong typing, it doesn't even provide (AFAIK) a mechanism for specifying types on caught exceptions, so promises with an any error type are consistent with how TypeScript handles errors in synchronous code.

    The comments section on this page about that very matter contains a comment that I think is very relevant here:

    By definition, an exception is an 'exceptional' condition, and could occur for a number of reason (e.g. syntax error, stack overflow, etc...). And while most of these errors do derive from the Error type, it is also possible something you call into could throw anything.

    The reason given on that same page for not supporting typed exceptions is also quite relevant here:

    Since we don't have any notion of what exceptions a function might throw, allowing a type annotation on 'catch' variable would be highly misleading - it's not an exception filter and it's not anything resembling a guarantee of type safety.

    So my advice would be not to try and pin down the error type in your type definition. Exceptions are unpredictable by their very nature, and the typed definition of .then is already hard enough to define as it is.


    Also to note: Many strongly-typed languages that include a promise-like structure also do nothing to express the type of potential errors they can produce. .NET's Task<T> has a single type parameter for the result, as does Scala's Future[T]. Scala's mechanism for encapsulating errors, Try[T] (which is a part of Future[T]'s interface) gives no guarantees on its resulting error type beyond inheriting from Throwable. So I would say that the single-type-parameter promises in TypeScript are in good company.