diff --git a/lib/web/cache/cache.js b/lib/web/cache/cache.js index 597de52bb4a..50c0eeaf0bc 100644 --- a/lib/web/cache/cache.js +++ b/lib/web/cache/cache.js @@ -4,7 +4,7 @@ const { kConstruct } = require('../../core/symbols') const { urlEquals, getFieldValues } = require('./util') const { kEnumerableProperty, isDisturbed } = require('../../core/util') const { webidl } = require('../fetch/webidl') -const { Response, cloneResponse, fromInnerResponse, getResponseState } = require('../fetch/response') +const { cloneResponse, fromInnerResponse, getResponseState } = require('../fetch/response') const { Request, fromInnerRequest, getRequestState } = require('../fetch/request') const { fetching } = require('../fetch/index') const { urlIsHttpHttpsScheme, createDeferredPromise, readAllBytes } = require('../fetch/util') @@ -268,7 +268,7 @@ class Cache { let innerRequest = null // 2. - if (request instanceof Request) { + if (webidl.is.Request(request)) { innerRequest = getRequestState(request) } else { // 3. innerRequest = getRequestState(new Request(request)) @@ -400,7 +400,7 @@ class Cache { */ let r = null - if (request instanceof Request) { + if (webidl.is.Request(request)) { r = getRequestState(request) if (r.method !== 'GET' && !options.ignoreMethod) { @@ -466,7 +466,7 @@ class Cache { // 2. if (request !== undefined) { // 2.1 - if (request instanceof Request) { + if (webidl.is.Request(request)) { // 2.1.1 r = getRequestState(request) @@ -748,7 +748,7 @@ class Cache { // 2. if (request !== undefined) { - if (request instanceof Request) { + if (webidl.is.Request(request)) { // 2.1.1 r = getRequestState(request) @@ -847,7 +847,10 @@ webidl.converters.MultiCacheQueryOptions = webidl.dictionaryConverter([ } ]) -webidl.converters.Response = webidl.interfaceConverter(Response) +webidl.converters.Response = webidl.interfaceConverter( + webidl.is.Response, + 'Response' +) webidl.converters['sequence'] = webidl.sequenceConverter( webidl.converters.RequestInfo diff --git a/lib/web/cookies/index.js b/lib/web/cookies/index.js index 23986aa148f..372112307bc 100644 --- a/lib/web/cookies/index.js +++ b/lib/web/cookies/index.js @@ -5,9 +5,7 @@ const { stringify } = require('./util') const { webidl } = require('../fetch/webidl') const { Headers } = require('../fetch/headers') -const headersToBrandCheck = globalThis.Headers - ? [Headers, globalThis.Headers] - : [Headers] +const brandChecks = webidl.brandCheckMultiple([Headers, globalThis.Headers].filter(Boolean)) /** * @typedef {Object} Cookie @@ -30,7 +28,7 @@ const headersToBrandCheck = globalThis.Headers function getCookies (headers) { webidl.argumentLengthCheck(arguments, 1, 'getCookies') - webidl.brandCheckMultiple(headers, headersToBrandCheck) + brandChecks(headers) const cookie = headers.get('cookie') @@ -57,7 +55,7 @@ function getCookies (headers) { * @returns {void} */ function deleteCookie (headers, name, attributes) { - webidl.brandCheckMultiple(headers, headersToBrandCheck) + brandChecks(headers) const prefix = 'deleteCookie' webidl.argumentLengthCheck(arguments, 2, prefix) @@ -82,7 +80,7 @@ function deleteCookie (headers, name, attributes) { function getSetCookies (headers) { webidl.argumentLengthCheck(arguments, 1, 'getSetCookies') - webidl.brandCheckMultiple(headers, headersToBrandCheck) + brandChecks(headers) const cookies = headers.getSetCookie() @@ -101,7 +99,7 @@ function getSetCookies (headers) { function setCookie (headers, cookie) { webidl.argumentLengthCheck(arguments, 2, 'setCookie') - webidl.brandCheckMultiple(headers, headersToBrandCheck) + brandChecks(headers) cookie = webidl.converters.Cookie(cookie) diff --git a/lib/web/fetch/body.js b/lib/web/fetch/body.js index 83426cd586a..99355d32e78 100644 --- a/lib/web/fetch/body.js +++ b/lib/web/fetch/body.js @@ -39,9 +39,9 @@ function extractBody (object, keepalive = false) { let stream = null // 2. If object is a ReadableStream object, then set stream to object. - if (object instanceof ReadableStream) { + if (webidl.is.ReadableStream(object)) { stream = object - } else if (object instanceof Blob) { + } else if (webidl.is.Blob(object)) { // 3. Otherwise, if object is a Blob object, set stream to the // result of running object’s get stream. stream = object.stream() @@ -64,7 +64,7 @@ function extractBody (object, keepalive = false) { } // 5. Assert: stream is a ReadableStream object. - assert(stream instanceof ReadableStream) + assert(webidl.is.ReadableStream(stream)) // 6. Let action be null. let action = null @@ -86,7 +86,7 @@ function extractBody (object, keepalive = false) { // Set type to `text/plain;charset=UTF-8`. type = 'text/plain;charset=UTF-8' - } else if (object instanceof URLSearchParams) { + } else if (webidl.is.URLSearchParams(object)) { // URLSearchParams // spec says to run application/x-www-form-urlencoded on body.list @@ -109,7 +109,7 @@ function extractBody (object, keepalive = false) { // Set source to a copy of the bytes held by object. source = new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength)) - } else if (object instanceof FormData) { + } else if (webidl.is.FormData(object)) { const boundary = `----formdata-undici-0${`${Math.floor(Math.random() * 1e11)}`.padStart(11, '0')}` const prefix = `--${boundary}\r\nContent-Disposition: form-data` @@ -178,7 +178,7 @@ function extractBody (object, keepalive = false) { // followed by the multipart/form-data boundary string generated // by the multipart/form-data encoding algorithm. type = `multipart/form-data; boundary=${boundary}` - } else if (object instanceof Blob) { + } else if (webidl.is.Blob(object)) { // Blob // Set source to object. @@ -206,7 +206,7 @@ function extractBody (object, keepalive = false) { } stream = - object instanceof ReadableStream ? object : ReadableStreamFrom(object) + webidl.is.ReadableStream(object) ? object : ReadableStreamFrom(object) } // 11. If source is a byte sequence, then set action to a @@ -265,7 +265,7 @@ function safelyExtractBody (object, keepalive = false) { // a byte sequence or BodyInit object object, run these steps: // 1. If object is a ReadableStream object, then: - if (object instanceof ReadableStream) { + if (webidl.is.ReadableStream(object)) { // Assert: object is neither disturbed nor locked. // istanbul ignore next assert(!util.isDisturbed(object), 'The body has already been consumed.') diff --git a/lib/web/fetch/formdata-parser.js b/lib/web/fetch/formdata-parser.js index 1890b27422f..4bbc2c909fe 100644 --- a/lib/web/fetch/formdata-parser.js +++ b/lib/web/fetch/formdata-parser.js @@ -4,6 +4,7 @@ const { isUSVString, bufferToLowerCasedHeaderName } = require('../../core/util') const { utf8DecodeBytes } = require('./util') const { HTTP_TOKEN_CODEPOINTS, isomorphicDecode } = require('./data-url') const { makeEntry } = require('./formdata') +const { webidl } = require('./webidl') const assert = require('node:assert') const { File: NodeFile } = require('node:buffer') @@ -204,7 +205,7 @@ function multipartFormDataParser (input, mimeType) { // 5.12. Assert: name is a scalar value string and value is either a scalar value string or a File object. assert(isUSVString(name)) - assert((typeof value === 'string' && isUSVString(value)) || value instanceof File) + assert((typeof value === 'string' && isUSVString(value)) || webidl.is.File(value)) // 5.13. Create an entry with name and value, and append it to entry list. entryList.push(makeEntry(name, value, filename)) diff --git a/lib/web/fetch/formdata.js b/lib/web/fetch/formdata.js index 4909e6dbc98..ef45cefb369 100644 --- a/lib/web/fetch/formdata.js +++ b/lib/web/fetch/formdata.js @@ -31,7 +31,7 @@ class FormData { name = webidl.converters.USVString(name) - if (arguments.length === 3 || value instanceof Blob) { + if (arguments.length === 3 || webidl.is.Blob(value)) { value = webidl.converters.Blob(value, prefix, 'value') if (filename !== undefined) { @@ -122,7 +122,7 @@ class FormData { name = webidl.converters.USVString(name) - if (arguments.length === 3 || value instanceof Blob) { + if (arguments.length === 3 || webidl.is.Blob(value)) { value = webidl.converters.Blob(value, prefix, 'value') if (filename !== undefined) { @@ -235,7 +235,7 @@ function makeEntry (name, value, filename) { // 1. If value is not a File object, then set value to a new File object, // representing the same bytes, whose name attribute value is "blob" - if (!(value instanceof File)) { + if (!webidl.is.File(value)) { value = new File([value], 'blob', { type: value.type }) } @@ -256,4 +256,6 @@ function makeEntry (name, value, filename) { return { name, value } } +webidl.is.FormData = webidl.util.MakeTypeAssertion(FormData.prototype) + module.exports = { FormData, makeEntry, setFormDataState } diff --git a/lib/web/fetch/index.js b/lib/web/fetch/index.js index b2c95d94a09..00d34071f7f 100644 --- a/lib/web/fetch/index.js +++ b/lib/web/fetch/index.js @@ -803,7 +803,7 @@ function schemeFetch (fetchParams) { // 2. If request’s method is not `GET`, blobURLEntry is null, or blobURLEntry’s // object is not a Blob object, then return a network error. - if (request.method !== 'GET' || !(blob instanceof Blob)) { + if (request.method !== 'GET' || !webidl.is.Blob(blob)) { return Promise.resolve(makeNetworkError('invalid method')) } @@ -1439,7 +1439,7 @@ async function httpNetworkOrCacheFetch ( // 11. If httpRequest’s referrer is a URL, then append // `Referer`/httpRequest’s referrer, serialized and isomorphic encoded, // to httpRequest’s header list. - if (httpRequest.referrer instanceof URL) { + if (webidl.is.URL(httpRequest.referrer)) { httpRequest.headersList.append('referer', isomorphicEncode(httpRequest.referrer.href), true) } diff --git a/lib/web/fetch/request.js b/lib/web/fetch/request.js index b6a0d6dfdfb..fec12b58308 100644 --- a/lib/web/fetch/request.js +++ b/lib/web/fetch/request.js @@ -144,7 +144,7 @@ class Request { // 6. Otherwise: // 7. Assert: input is a Request object. - assert(input instanceof Request) + assert(webidl.is.Request(input)) // 8. Set request to input’s request. request = input.#state @@ -490,7 +490,7 @@ class Request { // 33. Let inputBody be input’s request’s body if input is a Request // object; otherwise null. - const inputBody = input instanceof Request ? input.#state.body : null + const inputBody = webidl.is.Request(input) ? input.#state.body : null // 34. If either init["body"] exists and is non-null or inputBody is // non-null, and request’s method is `GET` or `HEAD`, then throw a @@ -986,23 +986,21 @@ Object.defineProperties(Request.prototype, { } }) +webidl.is.Request = webidl.util.MakeTypeAssertion(Request.prototype) + // https://fetch.spec.whatwg.org/#requestinfo webidl.converters.RequestInfo = function (V, prefix, argument) { if (typeof V === 'string') { return webidl.converters.USVString(V) } - if (V instanceof Request) { + if (webidl.is.Request(V)) { return V } return webidl.converters.USVString(V) } -webidl.converters.AbortSignal = webidl.interfaceConverter( - AbortSignal -) - // https://fetch.spec.whatwg.org/#requestinit webidl.converters.RequestInit = webidl.dictionaryConverter([ { diff --git a/lib/web/fetch/response.js b/lib/web/fetch/response.js index 904752f6c98..60cee8bb26e 100644 --- a/lib/web/fetch/response.js +++ b/lib/web/fetch/response.js @@ -19,7 +19,6 @@ const { nullBodyStatus } = require('./constants') const { webidl } = require('./webidl') -const { FormData } = require('./formdata') const { URLSerializer } = require('./data-url') const { kConstruct } = require('../../core/symbols') const assert = require('node:assert') @@ -569,7 +568,7 @@ webidl.converters.XMLHttpRequestBodyInit = function (V, prefix, name) { return webidl.converters.USVString(V, prefix, name) } - if (V instanceof Blob) { + if (webidl.is.Blob(V)) { return V } @@ -577,11 +576,11 @@ webidl.converters.XMLHttpRequestBodyInit = function (V, prefix, name) { return V } - if (V instanceof FormData) { + if (webidl.is.FormData(V)) { return V } - if (V instanceof URLSearchParams) { + if (webidl.is.URLSearchParams(V)) { return V } @@ -590,7 +589,7 @@ webidl.converters.XMLHttpRequestBodyInit = function (V, prefix, name) { // https://fetch.spec.whatwg.org/#bodyinit webidl.converters.BodyInit = function (V, prefix, argument) { - if (V instanceof ReadableStream) { + if (webidl.is.ReadableStream(V)) { return V } @@ -620,6 +619,8 @@ webidl.converters.ResponseInit = webidl.dictionaryConverter([ } ]) +webidl.is.Response = webidl.util.MakeTypeAssertion(Response.prototype) + module.exports = { isNetworkError, makeNetworkError, diff --git a/lib/web/fetch/util.js b/lib/web/fetch/util.js index daa2483c943..cb9f3dd5ba3 100644 --- a/lib/web/fetch/util.js +++ b/lib/web/fetch/util.js @@ -399,7 +399,7 @@ function determineRequestsReferrer (request) { // note: we need to clone it as it's mutated referrerSource = new URL(globalOrigin) - } else if (request.referrer instanceof URL) { + } else if (webidl.is.URL(request.referrer)) { // Let referrerSource be request’s referrer. referrerSource = request.referrer } @@ -476,7 +476,7 @@ function determineRequestsReferrer (request) { */ function stripURLForReferrer (url, originOnly) { // 1. Assert: url is a URL. - assert(url instanceof URL) + assert(webidl.is.URL(url)) url = new URL(url) @@ -508,7 +508,7 @@ function stripURLForReferrer (url, originOnly) { } function isURLPotentiallyTrustworthy (url) { - if (!(url instanceof URL)) { + if (!webidl.is.URL(url)) { return false } diff --git a/lib/web/fetch/webidl.js b/lib/web/fetch/webidl.js index 5397344e57b..34653e6439e 100644 --- a/lib/web/fetch/webidl.js +++ b/lib/web/fetch/webidl.js @@ -13,10 +13,12 @@ const NULL = 7 const OBJECT = 8 // function and object /** @type {import('../../../types/webidl').Webidl} */ -const webidl = {} -webidl.converters = {} -webidl.util = {} -webidl.errors = {} +const webidl = { + converters: {}, + util: {}, + errors: {}, + is: {} +} webidl.errors.exception = function (message) { return new TypeError(`${message.header}: ${message.message}`) @@ -43,18 +45,22 @@ webidl.errors.invalidArgument = function (context) { // https://webidl.spec.whatwg.org/#implements webidl.brandCheck = function (V, I) { - if (!(V instanceof I)) { + if (!I.prototype.isPrototypeOf(V)) { // eslint-disable-line no-prototype-builtins const err = new TypeError('Illegal invocation') err.code = 'ERR_INVALID_THIS' // node compat. throw err } } -webidl.brandCheckMultiple = function (V, List) { - if (List.every((clazz) => !(V instanceof clazz))) { - const err = new TypeError('Illegal invocation') - err.code = 'ERR_INVALID_THIS' // node compat. - throw err +webidl.brandCheckMultiple = function (List) { + const prototypes = List.map((c) => webidl.util.MakeTypeAssertion(c.prototype)) + + return (V) => { + if (prototypes.every(typeCheck => !typeCheck(V))) { + const err = new TypeError('Illegal invocation') + err.code = 'ERR_INVALID_THIS' // node compat. + throw err + } } } @@ -75,6 +81,11 @@ webidl.illegalConstructor = function () { }) } +const isPrototypeOf = Object.prototype.isPrototypeOf +webidl.util.MakeTypeAssertion = function (Prototype) { + return (O) => isPrototypeOf.call(Prototype, O) +} + // https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values webidl.util.Type = function (V) { switch (typeof V) { @@ -372,12 +383,12 @@ webidl.recordConverter = function (keyConverter, valueConverter) { } } -webidl.interfaceConverter = function (i) { +webidl.interfaceConverter = function (TypeCheck, name) { return (V, prefix, argument) => { - if (!(V instanceof i)) { + if (!TypeCheck(V)) { throw webidl.errors.exception({ header: prefix, - message: `Expected ${argument} ("${webidl.util.Stringify(V)}") to be an instance of ${i.name}.` + message: `Expected ${argument} ("${webidl.util.Stringify(V)}") to be an instance of ${name}.` }) } @@ -451,6 +462,14 @@ webidl.nullableConverter = function (converter) { } } +webidl.is.ReadableStream = webidl.util.MakeTypeAssertion(ReadableStream.prototype) +webidl.is.Blob = webidl.util.MakeTypeAssertion(Blob.prototype) +webidl.is.URLSearchParams = webidl.util.MakeTypeAssertion(URLSearchParams.prototype) +webidl.is.File = webidl.util.MakeTypeAssertion((globalThis.File ?? require('node:buffer').File).prototype) +webidl.is.URL = webidl.util.MakeTypeAssertion(URL.prototype) +webidl.is.AbortSignal = webidl.util.MakeTypeAssertion(AbortSignal.prototype) +webidl.is.MessagePort = webidl.util.MakeTypeAssertion(MessagePort.prototype) + // https://webidl.spec.whatwg.org/#es-DOMString webidl.converters.DOMString = function (V, prefix, argument, opts) { // 1. If V is null and the conversion is to an IDL type @@ -697,7 +716,12 @@ webidl.converters['record'] = webidl.recordConverter( webidl.converters.ByteString ) -webidl.converters.Blob = webidl.interfaceConverter(Blob) +webidl.converters.Blob = webidl.interfaceConverter(webidl.is.Blob, 'Blob') + +webidl.converters.AbortSignal = webidl.interfaceConverter( + webidl.is.AbortSignal, + 'AbortSignal' +) module.exports = { webidl diff --git a/lib/web/websocket/events.js b/lib/web/websocket/events.js index 760b7297359..d61f6e9634a 100644 --- a/lib/web/websocket/events.js +++ b/lib/web/websocket/events.js @@ -3,7 +3,6 @@ const { webidl } = require('../fetch/webidl') const { kEnumerableProperty } = require('../../core/util') const { kConstruct } = require('../../core/symbols') -const { MessagePort } = require('node:worker_threads') /** * @see https://html.spec.whatwg.org/multipage/comms.html#messageevent @@ -215,7 +214,10 @@ Object.defineProperties(ErrorEvent.prototype, { error: kEnumerableProperty }) -webidl.converters.MessagePort = webidl.interfaceConverter(MessagePort) +webidl.converters.MessagePort = webidl.interfaceConverter( + webidl.is.MessagePort, + 'MessagePort' +) webidl.converters['sequence'] = webidl.sequenceConverter( webidl.converters.MessagePort diff --git a/lib/web/websocket/stream/websocketerror.js b/lib/web/websocket/stream/websocketerror.js index ebe751efd71..e47551a96b9 100644 --- a/lib/web/websocket/stream/websocketerror.js +++ b/lib/web/websocket/stream/websocketerror.js @@ -78,4 +78,6 @@ Object.defineProperties(WebSocketError.prototype, { } }) +webidl.is.WebSocketError = webidl.util.MakeTypeAssertion(WebSocketError.prototype) + module.exports = { WebSocketError, createUnvalidatedWebSocketError } diff --git a/lib/web/websocket/stream/websocketstream.js b/lib/web/websocket/stream/websocketstream.js index 937d1ba4766..71d63a5767a 100644 --- a/lib/web/websocket/stream/websocketstream.js +++ b/lib/web/websocket/stream/websocketstream.js @@ -425,7 +425,7 @@ class WebSocketStream { let reasonString = '' // 3. If reason implements WebSocketError , - if (reason instanceof WebSocketError) { + if (webidl.is.WebSocketError(reason)) { // 3.1. Set code to reason ’s closeCode . code = reason.closeCode @@ -470,11 +470,6 @@ webidl.converters.WebSocketStreamOptions = webidl.dictionaryConverter([ } ]) -// TODO: take from request,hs -webidl.converters.AbortSignal ??= webidl.interfaceConverter( - AbortSignal -) - webidl.converters.WebSocketCloseInfo = webidl.dictionaryConverter([ { key: 'closeCode', diff --git a/lib/web/websocket/websocket.js b/lib/web/websocket/websocket.js index a0cd86c92c5..483b8c9f189 100644 --- a/lib/web/websocket/websocket.js +++ b/lib/web/websocket/websocket.js @@ -270,7 +270,7 @@ class WebSocket extends EventTarget { this.#sendQueue.add(data, () => { this.#bufferedAmount -= data.byteLength }, sendHints.typedArray) - } else if (data instanceof Blob) { + } else if (webidl.is.Blob(data)) { // If the WebSocket connection is established, and the WebSocket // closing handshake has not yet started, then the user agent must // send a WebSocket Message comprised of data using a binary frame @@ -666,7 +666,7 @@ webidl.converters['DOMString or sequence or WebSocketInit'] = functio webidl.converters.WebSocketSendData = function (V) { if (webidl.util.Type(V) === webidl.util.Types.OBJECT) { - if (V instanceof Blob) { + if (webidl.is.Blob(V)) { return V } diff --git a/test/webidl/helpers.js b/test/webidl/helpers.js index fcd375f68b4..fd7a7dfb97b 100644 --- a/test/webidl/helpers.js +++ b/test/webidl/helpers.js @@ -4,11 +4,13 @@ const { describe, test } = require('node:test') const assert = require('node:assert') const { webidl } = require('../../lib/web/fetch/webidl') -test('webidl.interfaceConverter', () => { +test('webidl.interfaceConverter', (t) => { class A {} class B {} - const converter = webidl.interfaceConverter(A) + const converter = webidl.interfaceConverter( + webidl.util.MakeTypeAssertion(A.prototype) + ) assert.throws(() => { converter(new B(), 'converter', 'converter') @@ -17,6 +19,19 @@ test('webidl.interfaceConverter', () => { assert.doesNotThrow(() => { converter(new A(), 'converter', 'converter') }) + + t.test('interfaceConverters ignore Symbol.hasInstance', () => { + class V {} + + Object.defineProperty(Blob.prototype, Symbol.hasInstance, { + value: () => true + }) + + const blobConverter = webidl.interfaceConverter(webidl.is.Blob, 'Blob') + + assert.throws(() => blobConverter(new V())) + assert.equal(webidl.is.Blob(new V()), false) + }) }) describe('webidl.dictionaryConverter', () => { diff --git a/types/webidl.d.ts b/types/webidl.d.ts index c42a2617fd2..ac73af521d7 100644 --- a/types/webidl.d.ts +++ b/types/webidl.d.ts @@ -1,4 +1,5 @@ // These types are not exported, and are only used internally +import * as undici from './index' /** * Take in an unknown value and return one that is of type T @@ -82,6 +83,8 @@ interface WebidlUtil { * Stringifies {@param V} */ Stringify (V: any): string + + MakeTypeAssertion (Prototype: T['prototype']): (arg: any) => arg is T } interface WebidlConverters { @@ -173,10 +176,27 @@ interface WebidlConverters { [Key: string]: (...args: any[]) => unknown } +type IsAssertion = (arg: any) => arg is T + +interface WebidlIs { + Request: IsAssertion + Response: IsAssertion + ReadableStream: IsAssertion + Blob: IsAssertion + URLSearchParams: IsAssertion + File: IsAssertion + FormData: IsAssertion + URL: IsAssertion + WebSocketError: IsAssertion + AbortSignal: IsAssertion + MessagePort: IsAssertion +} + export interface Webidl { errors: WebidlErrors util: WebidlUtil converters: WebidlConverters + is: WebidlIs /** * @description Performs a brand-check on {@param V} to ensure it is a @@ -184,7 +204,7 @@ export interface Webidl { */ brandCheck unknown>(V: unknown, cls: Interface): asserts V is Interface - brandCheckMultiple unknown)[]> (V: unknown, list: Interfaces): asserts V is Interfaces[number] + brandCheckMultiple unknown)[]> (list: Interfaces): (V: any) => asserts V is Interfaces[number] /** * @see https://webidl.spec.whatwg.org/#es-sequence @@ -207,11 +227,11 @@ export interface Webidl { * Similar to {@link Webidl.brandCheck} but allows skipping the check if third party * interfaces are allowed. */ - interfaceConverter (cls: Interface): ( + interfaceConverter (typeCheck: IsAssertion, name: string): ( V: unknown, prefix: string, argument: string - ) => asserts V is typeof cls + ) => asserts V is Interface // TODO(@KhafraDev): a type could likely be implemented that can infer the return type // from the converters given?