Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 362
Jason Kleban Avatar asked Mar 26 '15 18:03

Jason Kleban


2 Answers

Gee, this is a really hard one. It's actually a good question you're asking here.

Let me start with a

Yes, it can be typed

It's entirely possible to create a promise type which takes exceptions into account. When I implemented a promise library in a typed language, I started out with a Promise<T,E> type and only later reverted to Promise<T> - it worked but it wasn't fun. What you're asking for has a name.

Checked Exceptions

What you're actually asking for here is for exceptions to be checked - that is a function must declare the type of exceptions it might throw - there are languages that actually do this for exceptions well... there is a language that does it - Java. In Java, when you have a method that might throw an exception (except for a RuntimeException) it must declare it:

 public T foo() throws E {}

See, in Java - both the return type and the error type are a part of a method's signature. This is a controversial choice and a lot of people find it tedious. It's very unpopular among developers of other languages because it forces you to write a lot of crufty code.

Imagine you have a function that returns a promise that makes a db connection to get a URL, makes a web request and writes it to a file. The promise equivalent of what you have in Java is something like:

Promise<T, FileAccessError | DatabaseError | WebRequestError | WebConnectionError | TypeError>

Not a lot of fun to type many times - so the types of exceptions in these languages (like C#) is usually implicit. That said if you like that stylistic choice you definitely should do it. It's just not really trivial - the type of a promise is already pretty complicated:

then<T,U> :: Promise<T> -> T -> Promise<U> | U -> Promise<U>

This is what then does - it takes a promise of type T, and a callback that takes a T and returns a value (U) or a promise for a value (Promise) - and returns a Promise (unwrap and transform). The actual type is even harder since then has a second failure argument - and both arguments are optional:

then<T,U> :: Promise<T> -> (T -> Promise<U> | U) | null) -> ((Promise<T> -> any -> Promise<U> | U) | null) -> Promise<U>

If you add error handling in, this becomes really "fun" since all these steps now have an additional error path:

then<T,E,U,E2> :: Promise<T,E> -> (T -> Promise<U, E2> | U) | null -> (E -> Promise<U, E2> | U) | null -> Promise<U>

Basically - then now has 4 type parameters which is something people generally want to avoid :) It's entirely possible though and up to you.

like image 58
Benjamin Gruenbaum Avatar answered Nov 06 '22 06:11

Benjamin Gruenbaum


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.

like image 3
JLRishe Avatar answered Nov 06 '22 04:11

JLRishe