Define that interface objects are constructors
ES claims the following (twice):
Built-in function objects that are not identified as constructors do not implement the [[Construct]] internal method unless otherwise specified in the description of a particular function.
We don't normatively identify the interface object as a constructor, even if it has a [Constructor]. I guess we should.
A less obvious question is what we should do if there's no [Constructor]. Right now, I think we're aiming to install a [[Construct]] internal method that always throws, but that makes the interface object pass IsConstructor().
unless otherwise specified in the description of a particular function
This is the key, no? https://heycam.github.io/webidl/#Constructor specifies otherwise:
If the [Constructor] extended attribute appears on an interface, it indicates that the interface object for this interface will have an [[Construct]] internal method
So I think we're good here: thing with a [Constructor] have [[Construct]] and things without do not, as desired.
Though I guess that leaves open the question of the no-[Constructor] case. I just checked Gecko, and it does have a [[Constructor]] in that case, albeit one that always throws. I wonder whether the output of IsConstructor() is observable, other than by the exceptions that get thrown.
I guess you could do something where you use a DOM interface object as the .constructor of an array and see what happens in https://tc39.github.io/ecma262/#sec-arrayspeciescreate step 6.
And also whether a proxy for a DOM interface object ends up with a construct trap being called. So simple testcase:
<script>
var p = new Proxy(Node, { construct: () => console.log("constructing") });
new p()
</script>
On that testcase, Chrome and Firefox log "constructing"; Safari does not (and the exception it throws is about p not being a constructor). Yay interop. ;)
But OK, if we want to spec Chrome/Firefox behavior here then we need to change something in the idl spec.
And admittedly it would be good if the "is a constructor" bit was near the bit that defines the steps, not way off somewhere else...
I've always assumed they had constructors (and [[Construct]] methods), just throwing ones.
I think it'd be very strange to have callable interface objects exist at all, and be reachable via the .constructor property, if they are not also [[Construct]]able.
In practice it doesn't matter much, but I do prefer keeping [[Construct]] and just saying that it throws for things without a constructor in the IDL.
There was an interesting question about feature detection that was raised the other day: how to detect a change from "no [Constructor]" to "no-arg [Constructor]". Right now, I think "call the constructor and see if it throws" is the only way in Firefox and Chrome. The Proxy thing above could be used if we changed whether [[Construct]] happens...
But maybe this is enough of an edge case that it's not an issue.
@rniwa would Safari be open to changing its behavior here, or do you know whom I should be asking about it?
What's the benefit of implementing Chrome/Firefox behavior here?
FWIW, it seems there’s a number of ways to observe this distinction beyond just Proxy — pretty much anywhere IsConstructor is used, I guess. In addition to peeking out through various programmatic APIs, it can also impact evaluation of ‘bare’ syntax (third example below). Each of the following statements will throw a TypeError if Foo[[Construct]] is absent, but will not throw if it’s present:
Reflect.construct(Bar, [], Foo);
customElements.define('x-x', Foo);
class Bar extends Foo {}
Usually it’s throw vs don’t throw, but sometimes potentially neither throws but they still do other different things:
// If Foo[[Construct]] is present, (attempts) returning new Foo.
// If Foo[[Construct]] is absent, returns new Array.
Array.from.call(Foo, []);
// In practice here, the first case throws for the specific Web IDL issue under discussion,
// though it’s the reverse of the other cases (i.e. throw when [[Construct]] _is_ present).
I think it'd be very strange to have callable interface objects exist at all, and be reachable via the .constructor property, if they are not also [[Construct]]able.
I agree.
@rniwa would Safari be open to changing its behavior here, or do you know whom I should be asking about it?
This behavior was changed in JSDOMConstructorNotConstructable should be a constructor, aligning Safari with Chrome/Firefox.
Thanks @shvaikalesh! Do you know if there are web platform tests for this?
I looked into how we would update the Web IDL spec to be explicit about this, and it's still kinda unclear to me, largely because the ES spec is unclear. I will open an issue on tc39/ecma262 asking for guidance.
Do you know if there are web platform tests for this?
There are tests asserting that (class extends HTML___Element {}) doesn't throw, and that's it.
idlharness.js has a FIXME for implementing [[Construct]] check.