From aa66460f9b125a7070645f64f34a5574cd9eb549 Mon Sep 17 00:00:00 2001 From: "Marc J. Schmidt" Date: Fri, 27 Oct 2023 15:55:26 +0200 Subject: [PATCH] fix(type): correctly materialize Promise in runtime checks. This makes sure that the circular ref detector works correctly. fixes #495 --- packages/type/src/reflection/extends.ts | 22 ++++++++++----- packages/type/tests/type.spec.ts | 36 +++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/packages/type/src/reflection/extends.ts b/packages/type/src/reflection/extends.ts index 3a9ffe425..18264a3a1 100644 --- a/packages/type/src/reflection/extends.ts +++ b/packages/type/src/reflection/extends.ts @@ -11,7 +11,7 @@ import { addType, emptyObject, - flatten, + flatten, getTypeJitContainer, indexAccess, isMember, isOptional, @@ -29,7 +29,7 @@ import { TypeMethodSignature, TypeNumber, TypeObjectLiteral, - TypeParameter, + TypeParameter, TypePromise, TypeString, TypeTemplateLiteral, TypeTuple, @@ -98,8 +98,12 @@ export function _isExtendable(left: Type, right: Type, extendStack: StackEntry[] if (left.kind === ReflectionKind.promise && right.kind === ReflectionKind.object) return true; - if (left.kind === ReflectionKind.promise) return _isExtendable(createPromiseObjectLiteral(left.type), right); - if (right.kind === ReflectionKind.promise) return _isExtendable(left, createPromiseObjectLiteral(right.type)); + if (left.kind === ReflectionKind.promise) { + return _isExtendable(createPromiseObjectLiteral(left), right); + } + if (right.kind === ReflectionKind.promise) { + return _isExtendable(left, createPromiseObjectLiteral(right)); + } if (right.kind !== ReflectionKind.union) { if (left.kind === ReflectionKind.null) { @@ -329,8 +333,12 @@ export function _isExtendable(left: Type, right: Type, extendStack: StackEntry[] * We don't want to embed in each and every file the type definition of Promise, * so we do it ondemand at runtime instead. This saves bundle size. */ -export function createPromiseObjectLiteral(type: Type): TypeObjectLiteral { +export function createPromiseObjectLiteral(type: TypePromise): TypeObjectLiteral { + const jit = getTypeJitContainer(type); + if (jit.__promiseObjectLiteral) return jit.__promiseObjectLiteral; + const promise: TypeObjectLiteral = {} as any; + jit.__promiseObjectLiteral = promise; Object.assign(promise, { kind: ReflectionKind.objectLiteral, types: [ @@ -343,8 +351,8 @@ export function createPromiseObjectLiteral(type: Type): TypeObjectLiteral { type: { kind: ReflectionKind.union, types: [{ kind: ReflectionKind.function, parameters: [ - { kind: ReflectionKind.parameter, name: 'value', type: type }, - ], return: { kind: ReflectionKind.union, types: [type, { kind: ReflectionKind.promise, type: type }] } + { kind: ReflectionKind.parameter, name: 'value', type: type.type }, + ], return: { kind: ReflectionKind.union, types: [type.type, promise] } }, { kind: ReflectionKind.null }, { kind: ReflectionKind.undefined }] } }, diff --git a/packages/type/tests/type.spec.ts b/packages/type/tests/type.spec.ts index 780ec3d13..2ca847f82 100644 --- a/packages/type/tests/type.spec.ts +++ b/packages/type/tests/type.spec.ts @@ -1511,3 +1511,39 @@ test('issue-430: referring to this', () => { //this makes it possible that the code above works at least. expect(stringifyResolvedType(typeOf())).toBe(`'someFunctionA' | 'someFunctionB' | 'someFunctionC'`); }); + +test('issue-495: extend Promise in union', () => { + type Model = { + foo: string; + }; + + type Interface = { + modelBuilder(): Promise | Model; + }; + + class Implementation1 { + async modelBuilder(): Promise { + return {foo: 'bar'}; + } + } + + class Implementation2 { + async modelBuilder(): Promise<{foo2: string}> { + return {foo2: 'bar'}; + } + } + + { + type T = Implementation1 extends Interface ? true : false; + const type = typeOf(); + assertType(type, ReflectionKind.literal); + expect(type.literal).toBe(true); + } + + { + type T = Implementation2 extends Interface ? true : false; + const type = typeOf(); + assertType(type, ReflectionKind.literal); + expect(type.literal).toBe(false); + } +});