Developer advocate, full-stack engineer, startup co-founder & CTO, bringing 15 years of experience in Silicon Valley, including at Google and Yahoo!. Public speaker.
Location
🌐
Education
UC Santa Cruz Extension
Work
Developer Advocate at Weaviate, the open-source semantic search engine
How about the well-documented (since 2018) method of returning an Immediately-Invoked Async Function Expression from the constructor? Much simpler than the two workarounds, and more idiomatic than the static async factory functions. It's simply,
Well, we must first recall that async functions always return Promise instances. Hence, returning an async IIFE from a constructor is the same as if we returned a plain Promise, which is exactly what I argued against for Workaround #1. The main issue is that although this is acceptable according to the ECMAScript Standard (which allows arbitrary values to be returned from a constructor), it is sub-optimal in that it is surprising and even unexpected for most users—even to the TypeScript type inference engine! You can refer to the section about Workaround #1 for more details on why this is not ideal.
Developer advocate, full-stack engineer, startup co-founder & CTO, bringing 15 years of experience in Silicon Valley, including at Google and Yahoo!. Public speaker.
Location
🌐
Education
UC Santa Cruz Extension
Work
Developer Advocate at Weaviate, the open-source semantic search engine
I've read the 5 drawbacks of Workaround #1, but I don't quite see how they apply to the code I'm proposing. async-await works, there's no chaining of Promises, yes we use an arrow function to preserve this (is that a big deal?), and the last two points are unclear.
Ah, my apologies. I was not clear enough with my phrasing. What I meant is that the async IIFE you're proposing is semantically equivalent to the chained promises I presented in the article—just with some nice syntax. If we were to transform the async IIFE into their chained equivalent, then the points I made in Workaround #1 are still applicable—again, just with nicer syntax.
My main issue lies in the atypical semantics of returning Promise<this> (assuming such syntax exists) from the constructor. Normally, I expect that a class constructor returns an instance of that class. However, using async IIFEs (or chained promises otherwise) goes against this expectation since it does not return this but Promise<this> instead.
The deal breaker, then, is the fact that although this technique is allowed and correct in JavaScript, it is difficult to deny that it is unexpected unless documented. In my opinion, it is best to explicitly tell the user (through the type signature) that the function returns Promise<this> rather than relying on external documentation to know that the constructor produces Promise<this> rather than this.
This is exactly why I recommended static async functions: their type signature does not hide the fact that it returns Promise<this>. This is not the case for the constructor, which TypeScript assumes to (implicitly) return this, not Promise<this>.
my preferred solution as well, just return the result of the call to initialize from the constructor (and "this" from initialize)
you either have to document that the class has an asyncronous constructor, or, explain what factory function to call, i know what i prefer, oh and yes, if you include Async in the name of the class itself it will be hard to miss
Please be informed that you are just spamming around.
And your solution is not good at all. You cannot super() in the constructor and you cannot extends the class. And your constructor is not a constructor, which is harmful.
Developer advocate, full-stack engineer, startup co-founder & CTO, bringing 15 years of experience in Silicon Valley, including at Google and Yahoo!. Public speaker.
Location
🌐
Education
UC Santa Cruz Extension
Work
Developer Advocate at Weaviate, the open-source semantic search engine
Did you mean that more precisely, you can call super in the constructor; you can't await super() in derived class constructors, but you can extend the class if you don't need to overload the constructor? I've documented this downside in the SO answer.
Can you expand on how the constructor is not a constructor?
Pedantically, I am on the camp that constructors should never return anything at all. That's just an unexpected quirk of JavaScript that even tsserver trips up on.
Semantically, constructors initialize fields, not return values. Thus, returning a Promise from a constructor is not only awkward, but also unexpected because it returns something aside from the intended class instance (i.e., Promise<Animal> vs. Animal itself).
A static async factory method makes this distinction well: do the async work ahead of time and then invoke the constructor with the now-resolved field initializers.
How about the well-documented (since 2018) method of returning an Immediately-Invoked Async Function Expression from the constructor? Much simpler than the two workarounds, and more idiomatic than the static async factory functions. It's simply,
Well, we must first recall that
async
functions always returnPromise
instances. Hence, returning anasync
IIFE from a constructor is the same as if we returned a plainPromise
, which is exactly what I argued against for Workaround #1. The main issue is that although this is acceptable according to the ECMAScript Standard (which allows arbitrary values to be returned from aconstructor
), it is sub-optimal in that it is surprising and even unexpected for most users—even to the TypeScript type inference engine! You can refer to the section about Workaround #1 for more details on why this is not ideal.I've read the 5 drawbacks of Workaround #1, but I don't quite see how they apply to the code I'm proposing.
async-await
works, there's no chaining of Promises, yes we use an arrow function to preservethis
(is that a big deal?), and the last two points are unclear.Ah, my apologies. I was not clear enough with my phrasing. What I meant is that the
async
IIFE you're proposing is semantically equivalent to the chained promises I presented in the article—just with some nice syntax. If we were to transform theasync
IIFE into their chained equivalent, then the points I made in Workaround #1 are still applicable—again, just with nicer syntax.My main issue lies in the atypical semantics of returning
Promise<this>
(assuming such syntax exists) from theconstructor
. Normally, I expect that a class constructor returns an instance of that class. However, usingasync
IIFEs (or chained promises otherwise) goes against this expectation since it does not returnthis
butPromise<this>
instead.The deal breaker, then, is the fact that although this technique is allowed and correct in JavaScript, it is difficult to deny that it is unexpected unless documented. In my opinion, it is best to explicitly tell the user (through the type signature) that the function returns
Promise<this>
rather than relying on external documentation to know that theconstructor
producesPromise<this>
rather thanthis
.This is exactly why I recommended static
async
functions: their type signature does not hide the fact that it returnsPromise<this>
. This is not the case for theconstructor
, which TypeScript assumes to (implicitly) returnthis
, notPromise<this>
.Even if it's typed as
Awaited<ClassName>
?As far as I know, TypeScript does not allow the
constructor
to be directly type-annotated. As of writing, the compiler produces the following error:my preferred solution as well, just return the result of the call to initialize from the constructor (and "this" from initialize)
you either have to document that the class has an asyncronous constructor, or, explain what factory function to call, i know what i prefer, oh and yes, if you include Async in the name of the class itself it will be hard to miss
Please be informed that you are just spamming around.
And your solution is not good at all. You cannot
super()
in the constructor and you cannotextends
the class. And your constructor is not a constructor, which is harmful.Did you mean that more precisely, you can call
super
in the constructor; you can'tawait super()
in derived class constructors, but you can extend the class if you don't need to overload the constructor? I've documented this downside in the SO answer.Can you expand on how the constructor is not a constructor?
A constructor should anyway return the constructed object itself, instead of a promise resolving to the said object, unless the object is the promise.
we are discussing creating objects asyncronously, in that case the constructor HAS to return a promise.
Pedantically, I am on the camp that constructors should never return anything at all. That's just an unexpected quirk of JavaScript that even
tsserver
trips up on.Semantically, constructors initialize fields, not return values. Thus, returning a
Promise
from a constructor is not only awkward, but also unexpected because it returns something aside from the intendedclass
instance (i.e.,Promise<Animal>
vs.Animal
itself).A static async factory method makes this distinction well: do the
async
work ahead of time and then invoke theconstructor
with the now-resolved field initializers.Exactly. Constructors are even called
__init__
in Python (which is very close to JS regarding OOP design).