diff --git a/packages/frontend-internal/src/wiring/OpaqueType.test.ts b/packages/frontend-internal/src/wiring/OpaqueType.test.ts index 8e0ab0a3c8..eada974bbf 100644 --- a/packages/frontend-internal/src/wiring/OpaqueType.test.ts +++ b/packages/frontend-internal/src/wiring/OpaqueType.test.ts @@ -78,7 +78,8 @@ describe('OpaqueType', () => { // @ts-expect-error - internal field not accessible expect(myInstance.foo).toBe('bar'); - expect(OpaqueMyType.isInternal(myInstance)).toBe(true); + expect(OpaqueMyType.isType(myInstance)).toBe(true); + expect(OpaqueMyType.isType('hello')).toBe(false); const myInternal = OpaqueMyType.toInternal(myInstance); expect(myInternal).toBe(myInstance); @@ -87,12 +88,6 @@ describe('OpaqueType', () => { expect(myInternal.version).toBe('v1'); expect(myInternal.foo).toBe('bar'); - expect(OpaqueMyType.isVersion('v1', myInstance)).toBe(true); - expect(OpaqueMyType.isVersion('v2' as any, myInstance)).toBe(false); - - expect(OpaqueMyType.isInternal('hello')).toBe(false); - expect(OpaqueMyType.isVersion('v1', 'hello')).toBe(false); - expect(() => OpaqueMyType.toInternal('hello'), ).toThrowErrorMatchingInlineSnapshot( @@ -225,8 +220,9 @@ describe('OpaqueType', () => { // @ts-expect-error - internal field not accessible expect(myInstanceV2.bar).toBe('foo'); - expect(OpaqueMyType.isInternal(myInstanceV1)).toBe(true); - expect(OpaqueMyType.isInternal(myInstanceV2)).toBe(true); + expect(OpaqueMyType.isType(myInstanceV1)).toBe(true); + expect(OpaqueMyType.isType(myInstanceV2)).toBe(true); + expect(OpaqueMyType.isType('hello')).toBe(false); const myInternalV1 = OpaqueMyType.toInternal(myInstanceV1); expect(myInternalV1).toBe(myInstanceV1); @@ -247,20 +243,6 @@ describe('OpaqueType', () => { // Narrowing the version allows access to internal fields expect(myInternalV1.version === 'v1' && myInternalV1.foo).toBe('bar'); expect(myInternalV2.version === 'v2' && myInternalV2.bar).toBe('foo'); - - expect(OpaqueMyType.isVersion('v1', myInstanceV1)).toBe(true); - expect(OpaqueMyType.isVersion('v2', myInstanceV1)).toBe(false); - - expect(OpaqueMyType.isVersion('v1', myInstanceV2)).toBe(false); - expect(OpaqueMyType.isVersion('v2', myInstanceV2)).toBe(true); - - // Narrowing the version allows access to internal fields - expect(OpaqueMyType.isVersion('v1', myInstanceV1) && myInstanceV1.foo).toBe( - 'bar', - ); - expect(OpaqueMyType.isVersion('v2', myInstanceV2) && myInstanceV2.bar).toBe( - 'foo', - ); }); it('should support undefined version for backwards compatibility', () => { @@ -317,7 +299,8 @@ describe('OpaqueType', () => { // @ts-expect-error - internal field not accessible expect(myInstance.foo).toBe('bar'); - expect(OpaqueMyType.isInternal(myInstance)).toBe(true); + expect(OpaqueMyType.isType(myInstance)).toBe(true); + expect(OpaqueMyType.isType('hello')).toBe(false); const myInternal = OpaqueMyType.toInternal(myInstance); expect(myInternal).toBe(myInstance); @@ -325,12 +308,6 @@ describe('OpaqueType', () => { expect(myInternal.$$type).toBe('my-type'); expect(myInternal.version).toBe(undefined); expect(myInternal.foo).toBe('bar'); - - expect(OpaqueMyType.isVersion(undefined, myInstance)).toBe(true); - expect(OpaqueMyType.isVersion('v1' as any, myInstance)).toBe(false); - - expect(OpaqueMyType.isInternal('hello')).toBe(false); - expect(OpaqueMyType.isVersion(undefined, 'hello')).toBe(false); }); it('should support undefined version mixed with defined versions', () => { @@ -425,8 +402,9 @@ describe('OpaqueType', () => { // @ts-expect-error - internal field not accessible expect(myInstanceV2.bar).toBe('foo'); - expect(OpaqueMyType.isInternal(myInstanceV1)).toBe(true); - expect(OpaqueMyType.isInternal(myInstanceV2)).toBe(true); + expect(OpaqueMyType.isType(myInstanceV1)).toBe(true); + expect(OpaqueMyType.isType(myInstanceV2)).toBe(true); + expect(OpaqueMyType.isType('hello')).toBe(false); const myInternalV1 = OpaqueMyType.toInternal(myInstanceV1); expect(myInternalV1).toBe(myInstanceV1); @@ -447,19 +425,5 @@ describe('OpaqueType', () => { // Narrowing the version allows access to internal fields expect(myInternalV1.version === 'v1' && myInternalV1.foo).toBe('bar'); expect(myInternalV2.version === undefined && myInternalV2.bar).toBe('foo'); - - expect(OpaqueMyType.isVersion('v1', myInstanceV1)).toBe(true); - expect(OpaqueMyType.isVersion(undefined, myInstanceV1)).toBe(false); - - expect(OpaqueMyType.isVersion('v1', myInstanceV2)).toBe(false); - expect(OpaqueMyType.isVersion(undefined, myInstanceV2)).toBe(true); - - // Narrowing the version allows access to internal fields - expect(OpaqueMyType.isVersion('v1', myInstanceV1) && myInstanceV1.foo).toBe( - 'bar', - ); - expect( - OpaqueMyType.isVersion(undefined, myInstanceV2) && myInstanceV2.bar, - ).toBe('foo'); }); }); diff --git a/packages/frontend-internal/src/wiring/OpaqueType.ts b/packages/frontend-internal/src/wiring/OpaqueType.ts index 332843f6cc..5da3003418 100644 --- a/packages/frontend-internal/src/wiring/OpaqueType.ts +++ b/packages/frontend-internal/src/wiring/OpaqueType.ts @@ -28,7 +28,7 @@ export class OpaqueType< T extends { public: { $$type: string }; - versions: { version?: string } & Object; + versions: { version: string | undefined }; }, > { /** @@ -41,7 +41,7 @@ export class OpaqueType< static create< T extends { public: { $$type: string }; - versions: { version?: string } & Object; + versions: { version: string | undefined }; }, >(options: { type: T['public']['$$type']; @@ -78,48 +78,43 @@ export class OpaqueType< /** * @param value Input value expected to be an instance of this opaque type - * @throws If the value is not an instance of this opaque type + * @returns True if the value matches this opaque type + */ + isType(value: unknown): value is T['public'] { + return this.#isThisInternalType(value); + } + + /** + * @param value Input value expected to be an instance of this opaque type + * @throws If the value is not an instance of this opaque type or is of an unsupported version * @returns The internal version of the opaque type */ toInternal(value: unknown): T['public'] & T['versions'] { - if (!this.#isThisType(value)) { + if (!this.#isThisInternalType(value)) { throw new TypeError( `Invalid opaque type, expected '${ this.#type }', but got '${this.#stringifyUnknown(value)}'`, ); } - this.#throwIfInvalidVersion(value.version); - return value; - } - /** - * @param value Input value expected to be an instance of this opaque type - * @returns True if the value matches this opaque type - */ - isInternal(value: unknown): value is T['public'] & T['versions'] { - if (!this.#isThisType(value)) { - return false; + if (!this.#versions.has(value.version)) { + const expected = []; + if (this.#versions.has(undefined)) { + expected.push('undefined'); + } + const versions = Array.from(this.#versions).filter(Boolean); + if (versions.length > 0) { + expected.push(`one of ['${versions.join("', '")}']`); + } + throw new TypeError( + `Invalid opaque type instance, got version '${ + value.version + }', expected ${expected.join(' or ')}`, + ); } - this.#throwIfInvalidVersion(value.version); - return true; - } - /** - * @param version The expected version of the opaque type - * @param value Input value expected to be an instance of this opaque type - * @returns True if the value matches this opaque type and is the expected version - */ - isVersion( - version: TVersion, - value: unknown, - ): value is T['public'] & - (T['versions'] extends infer UVersion - ? UVersion extends { version: TVersion } - ? UVersion - : never - : never) { - return this.#isThisType(value) && value.version === version; + return value; } /** @@ -133,25 +128,7 @@ export class OpaqueType< return value as unknown as TBase; } - #throwIfInvalidVersion(version: string | undefined) { - if (!this.#versions.has(version)) { - const expected = []; - if (this.#versions.has(undefined)) { - expected.push('undefined'); - } - const versions = Array.from(this.#versions).filter(Boolean); - if (versions.length > 0) { - expected.push(`one of ['${versions.join("', '")}']`); - } - throw new TypeError( - `Invalid opaque type instance, got version '${version}', expected ${expected.join( - ' or ', - )}`, - ); - } - } - - #isThisType(value: unknown): value is T['public'] & T['versions'] { + #isThisInternalType(value: unknown): value is T['public'] & T['versions'] { if (value === null || typeof value !== 'object') { return false; }