Closed rotu closed 1 month ago
Note that instanceof
and such guards are not sufficient:
let s = Object.create(AbortSignal.prototype)
// s passes an instanceof check
console.assert(s instanceof AbortSignal)
// but s is still not admissable when expecting an AbortSignal
await fetch(
'http://example.com',
{signal: s}
) // runtime TypeError
Essentially a duplicate of #202, or at least requires that issue first.
Even if they went through the trouble of making AbortSignal
a declared class with a phantom private member, the error messages would still be "misleading" up until someone tried adding the private member and got an error message about private members and declarations. And someone who went through the trouble of trying to build an object from the scads of missing members would just work around the private member issue somehow, like with a type assertion. TS's type system just isn't set up for nominal types. Normally I'd say "use X workaround" but here it really looks like you're asking for #202.
Essentially a duplicate of #202, or at least requires that issue first.
Perhaps. That issue seems to be about user-defined types, whereas this is for platform-defined types.
Regardless, I think the machinery actually is in place to do limited nominal typing.
declare const PrimaryInterface : unique symbol
// https://webidl.spec.whatwg.org/#dfn-platform-object
type PlatformObject<in out idlInterface> = {
/** @internal */
readonly [PrimaryInterface] : idlInterface
}
type StructuralAbortSignal = AbortSignal
type NominalAbortSignal = PlatformObject<"AbortSignal">
type PlatformAbortSignal = NominalAbortSignal & StructuralAbortSignal
// this function will only check if signal is an "AbortSignal" - it won't suggest adding members to make it so
declare function myAbortable(signal: PlatformObject<"AbortSignal">):void;
myAbortable(null as any as PlatformAbortSignal) // okay
myAbortable(null as any as AbortSignal)
// Property '[PrimaryInterface]' is missing in type 'AbortSignal' but required in type 'PlatformObject<"AbortSignal">'.
myAbortable(null as any as {})
// Property '[PrimaryInterface]' is missing in type 'AbortSignal' but required in type 'PlatformObject<"AbortSignal">'.
myAbortable(null as any) // okay
And someone who went through the trouble of trying to build an object from the scads of missing members would just work around the private member issue somehow, like with a type assertion.
I would hope and expect that even with "proper" nominal typing support, we would still be able to use type assertions when we believe we know better than the type checker. That's not really an argument against using nominal typing here, even if we have to roll our own.
This is all my opinion, so feel free to ignore it.
I'm not trying to argue against using nominal typing, I'm saying that without officially supported nominal typing, TS's error messages imply object that type mismatches are structural in nature. They all mention that such-and-such members are missing. Currently the private
class member is the closest at giving you something that feels like a message about nominal types (I think if you manage to build your own class with all the same-named private members, you'll finally see a message that looks like "the private members are declared in different places"). Even your suggestion above with the unique symbol will give error messages about the fact that that property is missing (although it doesn't auto-suggest adding the property via IntelliSense).
Personally I think "you're missing a zillion members" is as likely to deter people as "you're missing this one magical member", and neither approach is going to stop the person we're imagining guarding against. Like, when you see an object type with a Date
member, would it even occur to you to try to build one structurally? Start with {}
and then debug your way to something TS accepts? Who is doing that for AbortSignal
? For people like this, we'd really need #202.
The problem actually is the missing "one magical member" ([[PrimaryInterface]] internal slot). @jcalz, what error message would you have?
e.g. FireFox's runtime message is:
TypeError: Window.fetch: 'signal' member of RequestInit does not implement interface AbortSignal.
Chrome:
TypeError: Failed to execute 'fetch' on 'Window': Failed to read the 'signal' property from 'RequestInit': Failed to convert value to 'AbortSignal'.
Safari:
FetchRequestInit.signal should be undefined, null or an AbortSignal object. This will throw in a future release.
The problem at runtime is the internal slot, which isn't the same as a TS type with a symbol-valued key, and someone calling your code with myAbortable({ [PrimaryInterface]: "AbortSignal" })
will pass the TS check and still fail at runtime. I think we all agree you can currently simulate nominal typing by adding a random structural thing somewhere, but presumably native nominal typing would give the error message like "you can't assign {β―}
to AbortSignal
. A value of type AbortSignal
can only come from the same declaration site as AbortSignal
. You cannot assemble one from an object literal." But that's all in #202.
Anyway, I think I might be repeating myself at this point and I'm not a TS team member so my opinion isn't going to move this issue in any direction. I'll bow out for now.
Yes, it's a hack. It's less of a hack than the current behavior: relying on the structural type of an AbortSignal
as a proxy for what is actually expected (a [[PrimaryInterface]]
internal slot). Indeed, an API expecting a platform object could have no structure from a javascript object perspective:
e.g.
const justASignal = Object.setPrototypeOf(AbortSignal.timeout(0), null)
fetch('http://example.com', {signal:justASignal})
I guess you could say that this is structural - it's just that the identifier is not exposed to the JavaScript runtime, and a symbol-typed key, which you're not going to dereference by accident, seems the closest equivalent in TypeScript today.
There are other internal slots (e.g. [ArrayBufferData]
, [PromiseState]
) and extended attributes (e.g. [Serializable]
, [Transferable]
) which would be handy to have from a type system perspective.
This issue has been marked as "Duplicate" and has seen no recent activity. It has been automatically closed for house-keeping purposes.
π Search Terms
WebIDL, Nominal, Structural, Interface, platform object
β Viability Checklist
β Suggestion
TypeScript should provide better support for "platform objects". Right now, it only checks them structurally, which:
Instead, it should:
unique symbol
types)π Motivating Example
Take the following code:
This emits the error:
aSignal
is NOT a platform object and that its "inherited interfaces" (in the WebIDL sense) does not includeAbortSignal
.AbortSignal
need not structurally examine the value.π» Use Cases