Skip to content

Commit

Permalink
Bugfix: modern decorator support
Browse files Browse the repository at this point in the history
  • Loading branch information
dpimonov committed Feb 17, 2024
1 parent e2f6800 commit 3ef0be0
Show file tree
Hide file tree
Showing 14 changed files with 286 additions and 31 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [7.2.1] - 2024-02-18

### Fixed

- TypeScript 5 decorators do not work.
- Circular loop when using multiple inheritance through the decorator type options.

## [7.2.0] - 2023-10-18

### Added
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ TypeScript needs to run with the `experimentalDecorators` and `emitDecoratorMeta

_If you want additional type-safety and reduced syntax you may wish to install [reflect-metadata](https://github.com/rbuckton/reflect-metadata). This step is on your choice and fully optional. When installed it must be available globally to work. This can usually be done with `import 'reflect-metadata';` in your main index file._

Starting from TypeScript 5 we also support modern decorator syntax. However because parameter decorations with the modern syntax at the time of this writing are not supported - you will not be able to use `Inject` decorator provided by the library as well as `reflect-metadata` package when decide to use modern syntax. We are going to add support as soon as it will be provided to us by the TypeScript. If you need `Inject` decorator and enabling legacy decorators support is not an option - you can use declarative configuration.

## How it works?

It defines configuration for each object which you are going to serialize or deserialize and uses this configuration to process data of your choice. There are two possible ways to define a configuration:
Expand Down
3 changes: 2 additions & 1 deletion spec/property.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Property, TypeManager, TypeSerializer } from '../src';
import { Property, Type, TypeManager, TypeSerializer } from '../src';

@Type()
class User
{
@Property() public name?: string;
Expand Down
9 changes: 5 additions & 4 deletions spec/use-cases/complex-polymorphic-types.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class Message
@Property(() => Messageable) public reciever: Messageable
@Property(String) public content: string

public constructor (content: string, sender: Messageable, reciever: Messageable)
public constructor(content: string, sender: Messageable, reciever: Messageable)
{
this.content = content;
this.sender = sender;
Expand All @@ -22,7 +22,7 @@ class Status
{
@Property(String) status: string;

public constructor (status: string)
public constructor(status: string)
{
this.status = status;

Expand All @@ -31,6 +31,7 @@ class Status

}

@Type()
abstract class Messageable
{
@Property(Array, [Message]) messages!: Message[];
Expand All @@ -56,7 +57,7 @@ class Chat extends Messageable implements HasTitle
{
@Property(String) title: string;

public constructor (title: string)
public constructor(title: string)
{
super();

Expand All @@ -75,7 +76,7 @@ class User extends Statusable implements Messageable, HasTitle
@Property(String) title: string;
@Property(Array, [Message]) messages!: Message[];

constructor (title: string)
public constructor(title: string)
{
super();

Expand Down
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export * from './generic-argument';
export * from './generic-metadata-resolver';
export * from './generic-metadata';
export * from './generic-structure';
export * from './inject-decorator';
export * from './inject-index';
export * from './inject-internals';
export * from './inject-metadata';
Expand All @@ -33,6 +34,7 @@ export * from './log-level';
export * from './log';
export * from './metadata';
export * from './naming-convention';
export * from './property-decorator';
export * from './property-extension-metadata-ctor-set-key';
export * from './property-extension-metadata-ctor';
export * from './property-extension-metadata';
Expand All @@ -58,6 +60,7 @@ export * from './type-configuration';
export * from './type-context-entry';
export * from './type-context';
export * from './type-ctor';
export * from './type-decorator';
export * from './type-extension-metadata-ctor-set-key';
export * from './type-extension-metadata-ctor';
export * from './type-extension-metadata';
Expand All @@ -74,5 +77,6 @@ export * from './type-name';
export * from './type-options-base';
export * from './type-options';
export * from './type-resolver';
export * from './type-scope';
export * from './type';
export * from './unknown';
6 changes: 6 additions & 0 deletions src/inject-decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Inject decorator.
*
* @type {InjectDecorator}
*/
export type InjectDecorator = (target: any, propertyName: string | symbol | undefined, injectIndex: number) => void;
9 changes: 5 additions & 4 deletions src/inject.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { isFunction, isNumber, isObject, isString, isUndefined } from 'lodash';
import { isCtorFunction } from './functions/is-ctor-function';
import { nameOf } from './functions/name-of';
import { InjectDecorator } from './inject-decorator';
import { InjectOptions } from './inject-options';
import { TypeFn } from './type-fn';
import { TypeManager } from './type-manager';
Expand All @@ -10,9 +11,9 @@ import { TypeManager } from './type-manager';
*
* @param {TypeFn<TType>|InjectOptions<TType>|string} x Type function, inject options or parameter key from type context.
*
* @returns {ParameterDecorator} Parameter decorator.
* @returns {InjectDecorator} Inject decorator.
*/
export function Inject<TType>(x: TypeFn<TType> | InjectOptions<TType> | string): ParameterDecorator
export function Inject<TType>(x: TypeFn<TType> | InjectOptions<TType> | string): InjectDecorator
{
const injectOptions = (isObject(x) ? x : {}) as InjectOptions<TType>;

Expand All @@ -25,8 +26,8 @@ export function Inject<TType>(x: TypeFn<TType> | InjectOptions<TType> | string):
{
injectOptions.typeFn = x as TypeFn<TType>;
}

return function (target: any, propertyName: string | symbol, injectIndex: number): void
return function (target: any, propertyName: string | symbol | undefined, injectIndex: number): void
{
if (!isCtorFunction(target))
{
Expand Down
6 changes: 6 additions & 0 deletions src/property-decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Property decorator.
*
* @type {PropertyDecorator}
*/
export type PropertyDecorator = (target: any, context: any) => any;
75 changes: 60 additions & 15 deletions src/property.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { isArray, isFunction, isObject, isString, isSymbol, merge } from 'lodash';
import { isArray, isFunction, isObject, isString, isSymbol, isUndefined, merge } from 'lodash';
import { isCtorFunction } from './functions/is-ctor-function';
import { nameOf } from './functions/name-of';
import { GenericArgument } from './generic-argument';
import { PropertyDecorator } from './property-decorator';
import { PropertyOptions } from './property-options';
import { TypeArgument } from './type-argument';
import { TypeFn } from './type-fn';
Expand Down Expand Up @@ -54,27 +55,71 @@ export function Property<TType>(
propertyOptions.typeArgument = x;
}

return function (target: any, propertyName: string | symbol): void
return function (target: any, context: any): any
{
if (isCtorFunction(target))
// Modern decorator has a dynamic target which is dependent from where decorator
// is applied (target), context as a second parameter (context) and optional
// resolver like return type.
if (isObject(context) && context.hasOwnProperty('kind'))
{
throw new Error(`${nameOf(target)}.${String(propertyName)}: property decorator cannot be applied to a static member.`);
}
const decoratorContext = context as any;
const kind = decoratorContext.kind;
const propertyName = decoratorContext.name;

if (isSymbol(propertyName))
{
throw new Error(`${nameOf(target.constructor)}.${String(propertyName)}: property decorator cannot be applied to a symbol.`);
if (kind === 'method' || kind === 'class')
{
throw new Error(`${String(propertyName)}: property decorator cannot be applied to a method or a class.`);
}

if (isUndefined(propertyName))
{
throw new Error(`${String(propertyName)}: property decorator cannot be applied to undefined values.`);
}

if (isSymbol(propertyName))
{
throw new Error(`${String(propertyName)}: property decorator cannot be applied to a symbol.`);
}

TypeManager.typeScope.addPropertyOptions(propertyName, propertyOptions);

return;
}

if (isFunction(target[propertyName]))
// Legacy decorator has class reference as a first parameter (target), property name
// or symbol as a second parameter (context) and no return type.
if (isObject(target) && (isString(context) || isSymbol(context)))
{
throw new Error(`${nameOf(target.constructor)}.${String(propertyName)}: property decorator cannot be applied to a method.`);
}
const legacyTarget = target as any;
const propertyName = context as string | symbol | undefined;

if (isCtorFunction(legacyTarget))
{
throw new Error(`${nameOf(legacyTarget)}.${String(propertyName)}: property decorator cannot be applied to a static member.`);
}

const typeFn = target.constructor as TypeFn<any>;

TypeManager.configureTypeMetadata(typeFn).configurePropertyMetadata(propertyName, propertyOptions);
if (isUndefined(propertyName))
{
throw new Error(`${nameOf(legacyTarget)}.${String(propertyName)}: property decorator cannot be applied to undefined values.`);
}

if (isSymbol(propertyName))
{
throw new Error(`${nameOf(legacyTarget.constructor)}.${String(propertyName)}: property decorator cannot be applied to a symbol.`);
}

if (isFunction(legacyTarget[propertyName]))
{
throw new Error(`${nameOf(legacyTarget.constructor)}.${String(propertyName)}: property decorator cannot be applied to a method.`);
}

const typeFn = legacyTarget.constructor as TypeFn<any>;

TypeManager.configureTypeMetadata(typeFn).configurePropertyMetadata(propertyName, propertyOptions);

return;
}

return;
throw new Error(`Property decorator was not able to detect correct resolver for the following target [${target}] and context [${context}].`);
};
}
6 changes: 6 additions & 0 deletions src/type-decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Type decorator.
*
* @type {TypeDecorator}
*/
export type TypeDecorator = (target: any, context?: any) => any;
8 changes: 8 additions & 0 deletions src/type-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { TypeMetadata } from './type-metadata';
import { typeMetadataSymbol } from './type-metadata-symbol';
import { TypeOptions } from './type-options';
import { TypeOptionsBase } from './type-options-base';
import { TypeScope } from './type-scope';
import { Unknown } from './unknown';

/**
Expand Down Expand Up @@ -109,6 +110,13 @@ export class TypeManager
*/
public static readonly staticTypeManager: TypeManager = new TypeManager();

/**
* Type scope to work with registration in static context.
*
* @type {TypeScope}
*/
public static readonly typeScope: TypeScope = new TypeScope();

/**
* Type function map for types with aliases.
*
Expand Down
15 changes: 11 additions & 4 deletions src/type-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1121,11 +1121,18 @@ export class TypeMetadata<TType> extends Metadata
for (const parentTypeFn of parentTypeFns)
{
const parentTypeMetadata = this.typeManager.extractTypeMetadata(parentTypeFn);

this.parentTypeMetadatas.push(parentTypeMetadata);
this.typeOptions.parentTypeFns.push(parentTypeFn);
}

if (!this.parentTypeMetadatas.some(ptm => ptm === parentTypeMetadata))
{
this.parentTypeMetadatas.push(parentTypeMetadata);
}

if (!this.typeOptions.parentTypeFns.some(ptf => ptf === parentTypeFn))
{
this.typeOptions.parentTypeFns.push(parentTypeFn);
}
}

this.deriveParentTypeMetadataProperties();
this.hasDiscriminant(this.discriminant);

Expand Down
Loading

0 comments on commit 3ef0be0

Please sign in to comment.