diff --git a/packages/type/src/reflection/processor.ts b/packages/type/src/reflection/processor.ts index 4bbd85d39..eb39142d4 100644 --- a/packages/type/src/reflection/processor.ts +++ b/packages/type/src/reflection/processor.ts @@ -57,7 +57,7 @@ import { MappedModifier, ReflectionOp } from '@deepkit/type-spec'; import { isExtendable } from './extends.js'; import { ClassType, isArray, isClass, isFunction, stringifyValueWithType } from '@deepkit/core'; import { isWithDeferredDecorators } from '../decorator.js'; -import { ReflectionClass, TData } from './reflection.js'; +import { extractTypeNameFromFunction, ReflectionClass, TData } from './reflection.js'; import { state } from './state.js'; export type RuntimeStackEntry = Type | Object | (() => ClassType | Object) | string | number | boolean | bigint; @@ -111,26 +111,18 @@ export function unpack(pack: Packed): PackStruct { return { ops, stack: pack.length > 1 ? pack.slice(0, -1) : [] }; } -export function resolvePacked(type: Packed, args: any[] = []): Type { - return resolveRuntimeType(type, args) as Type; +export function resolvePacked(type: Packed, args: any[] = [], options?: ReflectOptions): Type { + return resolveRuntimeType(type, args, options) as Type; } function isPack(o: any): o is Packed { return isArray(o); } -function extractTypeNameFromFunction(fn: Function): string { - const str = fn.toString(); - //either it starts with __Ω* or __\u{3a9}* (bun.js) - const match = str.match(/(?:__Ω|__\\u\{3a9\})([\w]+)/); - if (match) { - return match[1]; - } - return 'UnknownTypeName:' + str; -} - /** * Computes a type of given object. This function caches the result on the object itself. + * This is the slow path, using the full type virtual machine to resolve the type. + * If you want to handle some fast paths (including cache), try using resolveReceiveType() instead. */ export function resolveRuntimeType(o: ClassType | Function | Packed | any, args: any[] = [], options?: ReflectOptions): Type { const type = Processor.get().reflect(o, args, options || { reuseCached: true }); diff --git a/packages/type/src/reflection/reflection.ts b/packages/type/src/reflection/reflection.ts index 4c48cb7d4..7a457de01 100644 --- a/packages/type/src/reflection/reflection.ts +++ b/packages/type/src/reflection/reflection.ts @@ -51,7 +51,7 @@ import { TypeTemplateLiteral } from './type.js'; import { AbstractClassType, arrayRemoveItem, ClassType, getClassName, isArray, isClass, isPrototypeOfBase, stringifyValueWithType } from '@deepkit/core'; -import { Packed, resolvePacked, resolveRuntimeType } from './processor.js'; +import { Packed, resolvePacked, resolveRuntimeType, unpack } from './processor.js'; import { NoTypeReceived } from '../utils.js'; import { findCommonLiteral } from '../inheritance.js'; import type { ValidateFunction } from '../validator.js'; @@ -75,11 +75,25 @@ export type ReceiveType = Packed | Type | ClassType; export function resolveReceiveType(type?: Packed | Type | ClassType | AbstractClassType | ReflectionClass): Type { if (!type) throw new NoTypeReceived(); + let typeFn: Function | undefined = undefined; + + if (isArray(type)){ + if (type.__type) return type.__type; + // this is fast path for simple references to a type, e.g. cast(), so that User is directly handled + // instead of running the VM to resolve to User first. + if (type[type.length - 1] === 'n!') { + //n! represents a simple inline: [Op.inline, 0] + typeFn = (type as any)[0] as Function; + type = typeFn() as Packed | Type | ClassType | AbstractClassType | ReflectionClass; + } + } + if (type instanceof ReflectionClass) return type.type; if (isArray(type) && type.__type) return type.__type; if (isType(type)) return type as Type; if (isClass(type)) return resolveRuntimeType(type) as Type; - return resolvePacked(type); + const typeName = typeFn ? extractTypeNameFromFunction(typeFn) : undefined; + return resolvePacked(type, undefined, {reuseCached: true, typeName}); } export function reflect(o: any, ...args: any[]): Type { @@ -1419,6 +1433,17 @@ export class ReflectionClass { } } +export function extractTypeNameFromFunction(fn: Function): string { + const str = fn.toString(); + //either it starts with __Ω* or __\u{3a9}* (bun.js) + const match = str.match(/(?:__Ω|__\\u\{3a9\})([\w]+)/); + if (match) { + return match[1]; + } + return 'UnknownTypeName:' + str; +} + + // old function to decorate an interface // export function decorate(decorate: { [P in keyof T]?: FreeDecoratorFn }, p?: ReceiveType): ReflectionClass { // const type = typeOf([], p); diff --git a/packages/type/src/reflection/type.ts b/packages/type/src/reflection/type.ts index 18aaae763..f259c2755 100644 --- a/packages/type/src/reflection/type.ts +++ b/packages/type/src/reflection/type.ts @@ -13,6 +13,7 @@ import { TypeNumberBrand } from '@deepkit/type-spec'; import { getProperty, ReceiveType, reflect, ReflectionClass, resolveReceiveType, toSignature } from './reflection.js'; import { isExtendable } from './extends.js'; import { state } from './state.js'; +import { resolveRuntimeType } from './processor.js'; export enum ReflectionVisibility { public, @@ -2743,6 +2744,6 @@ export function stringifyType(type: Type, stateIn: Partial export function annotateClass(clazz: ClassType | AbstractClassType, type?: ReceiveType) { (clazz as any).__type = isClass(type) ? (type as any).__type || [] : []; - type = resolveReceiveType(type); + type = resolveRuntimeType(type); (clazz as any).__type.__type = type; } diff --git a/packages/type/tests/receive-type.spec.ts b/packages/type/tests/receive-type.spec.ts index 522497037..e9634aa61 100644 --- a/packages/type/tests/receive-type.spec.ts +++ b/packages/type/tests/receive-type.spec.ts @@ -1,6 +1,7 @@ import { expect, test } from '@jest/globals'; import { ReceiveType, resolveReceiveType, typeOf } from '../src/reflection/reflection.js'; import { ReflectionKind, Type } from '../src/reflection/type.js'; +import { ReflectionOp } from '@deepkit/type-spec'; test('typeOf', () => { const type = typeOf(); @@ -30,6 +31,17 @@ test('method call', () => { expect(type).toEqual({ kind: ReflectionKind.string }); }); +test('maintain name', () => { + interface User {} + + function c(type?: ReceiveType) { + return resolveReceiveType(type); + } + + const t = c(); + expect(t.typeName).toBe('User'); +}); + test('decorator call', () => { let got: Type | undefined; diff --git a/packages/type/tests/validation.spec.ts b/packages/type/tests/validation.spec.ts index e04f1a28b..e6119ae2c 100644 --- a/packages/type/tests/validation.spec.ts +++ b/packages/type/tests/validation.spec.ts @@ -1,10 +1,10 @@ -import { expect, test, jest } from '@jest/globals'; +import { expect, jest, test } from '@jest/globals'; import { Email, MaxLength, MinLength, Positive, Validate, validate, validates, ValidatorError } from '../src/validator.js'; import { assert, is } from '../src/typeguard.js'; import { AutoIncrement, Excluded, Group, integer, PrimaryKey, Type, Unique } from '../src/reflection/type.js'; import { t } from '../src/decorator.js'; import { ReflectionClass, typeOf } from '../src/reflection/reflection.js'; -import { validatedDeserialize } from '../src/serializer-facade.js'; +import { cast, castFunction, validatedDeserialize } from '../src/serializer-facade.js'; test('primitives', () => { expect(validate('Hello')).toEqual([]); @@ -163,7 +163,12 @@ test('path', () => { // expect(validate({ configs: [{ name: 'a', value: 3 }] })).toEqual([]); expect(validate({ configs: {} })).toEqual([{ code: 'type', message: 'Not an array', path: 'configs', value: {} }]); - expect(validate({ configs: [{ name: 'a', value: 123 }, { name: '12' }] })).toEqual([{ code: 'type', message: 'Not a number', path: 'configs.1.value', value: undefined }]); + expect(validate({ configs: [{ name: 'a', value: 123 }, { name: '12' }] })).toEqual([{ + code: 'type', + message: 'Not a number', + path: 'configs.1.value', + value: undefined + }]); class Container2 { configs: { [name: string]: Config } = {}; @@ -179,7 +184,12 @@ test('class with union literal', () => { } expect(validate({ readConcernLevel: 'majority' })).toEqual([]); - expect(validate({ readConcernLevel: 'invalid' })).toEqual([{ code: 'type', message: 'No valid union member found. Valid: \'local\' | \'majority\' | \'linearizable\' | \'available\'', path: 'readConcernLevel', value: 'invalid' }]); + expect(validate({ readConcernLevel: 'invalid' })).toEqual([{ + code: 'type', + message: 'No valid union member found. Valid: \'local\' | \'majority\' | \'linearizable\' | \'available\'', + path: 'readConcernLevel', + value: 'invalid' + }]); }); test('named tuple', () => { @@ -269,10 +279,12 @@ test('inline object', () => { test('readonly constructor properties', () => { class Pilot { - constructor(readonly name: string, readonly age: number) {} + constructor(readonly name: string, readonly age: number) { + } } - expect(validate({name: 'Peter', age: 32})).toEqual([]); - expect(validate({name: 'Peter', age: 'sdd'})).toEqual([{code: 'type', message: 'Not a number', path: 'age', value: 'sdd'}]); + + expect(validate({ name: 'Peter', age: 32 })).toEqual([]); + expect(validate({ name: 'Peter', age: 'sdd' })).toEqual([{ code: 'type', message: 'Not a number', path: 'age', value: 'sdd' }]); }); test('class with statics', () => { @@ -287,8 +299,8 @@ test('class with statics', () => { } } - expect(validate({value: 34})).toEqual([]); - expect(validate({value: '33'})).toEqual([{code: 'type', message: 'Not a number', path: 'value', value: '33'}]); + expect(validate({ value: 34 })).toEqual([]); + expect(validate({ value: '33' })).toEqual([{ code: 'type', message: 'Not a number', path: 'value', value: '33' }]); }); test('date', () => { @@ -296,5 +308,49 @@ test('date', () => { public name!: string; public createdAt!: Date; } - expect(validate({name: "jack", createdAt: 'asd'})).toEqual([{code: 'type', message: 'Not a Date', path: 'createdAt', value: 'asd'}]); + + expect(validate({ name: 'jack', createdAt: 'asd' })).toEqual([{ code: 'type', message: 'Not a Date', path: 'createdAt', value: 'asd' }]); +}); + +test('speed', () => { + interface User { + username: string; + logins: number; + created: string; + } + + let start, end; + + const iterations = 100_000; + + const c = castFunction(); + start = performance.now(); + for (let i = 1; i < iterations; i++) { + const user = c({ + username: 'Peter', + logins: 23, + created: '2020-01-01', + }); + } + end = performance.now(); + const deepkitInlined = end - start; + console.log('deepkitInlined', deepkitInlined); + + start = performance.now(); + for (let i = 1; i < iterations; i++) { + const user = cast({ + username: 'Peter', + logins: 23, + created: '2020-01-01', + }); + } + end = performance.now(); + const deepkitRef = end - start; + console.log('deepkitRef', deepkitRef); + + const ratio = deepkitRef / deepkitInlined; + console.log('ratio', ratio); + // there is a performance cost involved in passing User to cast, + // but inline vs ref must not be slower than 2.5 times. + expect(ratio).toBeLessThan(2.5); });