frontend-internal: refactor OpaqueType to just isType + toInternal

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-09-11 13:14:30 +02:00
parent b17ad1b745
commit 8c7a8d41d2
2 changed files with 38 additions and 97 deletions
@@ -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');
});
});
@@ -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<TVersion extends T['versions']['version']>(
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;
}