Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve annotations #1343

Merged
merged 37 commits into from
May 14, 2021
Merged

Conversation

tonyhallett
Copy link
Contributor

@tonyhallett tonyhallett commented May 8, 2021

Description

I have created a new function that is used for all the decorators that are applicable to parameters and properties ( including accessors ).

//e.g
function multiInject(serviceIdentifier: interfaces.ServiceIdentifier<any>) {
  return createTaggedDecorator(new Metadata(METADATA_KEY.MULTI_INJECT_TAG, serviceIdentifier));
}

function createTaggedDecorator(
    metadata:interfaces.MetadataOrMetadataArray,
    callback?:(target: any, targetKey: string, index?: number | PropertyDescriptor) => void
) {
    return function(target: any, targetKey: string, index?: number | PropertyDescriptor) {
        if(callback){
            callback(target, targetKey, index);
        }
        if (typeof index === "number") {
            tagParameter(target, targetKey, index, metadata);
        } else {
            tagProperty(target, targetKey, metadata);
        }
    };
}

I have also exported createTaggedDecorator from inversify.ts to be available for anyone to use.
Note that this function allows multiple metadata to be provided through a single decorator.

I have also refactored _tagParameterOrProperty as it was checking if it was being applied for a parameter or a property. Now it is passed only the information that it requires.

I also changed the type to include undefined
let paramOrPropertyMetadata: interfaces.Metadata[] | undefined = paramsOrPropertiesMetadata[key];
and then checked against undefined.

Related Issue

#1342

Motivation and Context

#1342

How Has This Been Tested?

Properly

  • Updated docs / Refactor code / Added a tests case (non-breaking change)
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • All new and existing tests passed.
  • I have updated the changelog.

src/inversify.ts Show resolved Hide resolved
src/annotation/inject.ts Outdated Show resolved Hide resolved
Copy link
Contributor Author

@tonyhallett tonyhallett left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps could permit inject and multi inject to have two addditional parameters as a shortcut for the named and tagged decorators.

Copy link
Member

@notaphplover notaphplover left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks great, I just added some comments

src/utils/js.ts Outdated Show resolved Hide resolved
src/utils/js.ts Outdated Show resolved Hide resolved
src/inversify.ts Show resolved Hide resolved
@tonyhallett tonyhallett marked this pull request as ready for review May 9, 2021 09:09
Copy link
Member

@notaphplover notaphplover left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, we only need to update the changelog and this PR should be ready :)

Copy link
Member

@notaphplover notaphplover left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! I just added some comments :)

src/annotation/decorator_utils.ts Outdated Show resolved Hide resolved
src/annotation/decorator_utils.ts Outdated Show resolved Hide resolved
@tonyhallett
Copy link
Contributor Author

I have one other commit to come regarding an unhelpful error message that can arise for decorated properties missing inject / multi inject.

@PodaruDragos
Copy link
Contributor

@tonyhallett that introduces another problem for eslint.
I know we can disable some rules to our fitting but using Object will throw this error:

Don't use `Object` as a type. The `Object` type actually means "any non-nullish value", so it is marginally better than `unknown`.
- If you want a type meaning "any object", you probably want `Record<string, unknown>` instead.
- If you want a type meaning "any value", you probably want `unknown` instead

link

Copy link
Member

@notaphplover notaphplover left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What should we do with


    export interface MetadataReader {
        getPropertiesMetadata(constructorFunc: Function): MetadataMap;
    }

Given that symbols can be keys ?

I would not change it. It's something Typescript has to fix in order to allow symbol index signatures

Should the wiki have an example with symbol property injection ?

Probably, but that's something we could add in another PR

Are you happy with the name of the additional Target property identifier ?

I'm not sure, do we need it? Maybe I'm missing something

src/planning/target.ts Outdated Show resolved Hide resolved

export function injectBase(metadataKey:string){
return (serviceIdentifier: ServiceIdentifierOrFunc) => {
return (target: Object,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Record<string | symbol, unknown> | interfaces.Newable<unknown> is a better type for target

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not acceptable by the compiler

is not assignable to type 'Record<string | symbol, unknown>'. Index signature is missing in type

I think that I have provided a solution

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interface ConstructorFunction {
    new(...args:any[]):unknown,
    readonly prototype:unknown
}
interface Prototype {
    constructor: Function;
}
export type DecoratorTarget = Prototype | ConstructorFunction;

Copy link
Member

@notaphplover notaphplover May 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's indeed a valid solution. We want to introduce some linter rules in the future and that's the reason to ban some types. If you want I can create a PR to your branch with equivalent types which respect this rule :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you elaborate on CI ?

By all means change the typing. I hadn't seen the banning of Function. I look forward to seeing the equivalent types.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can also just add this into out tslint setup right now and then just try out what works best, since ban-types are somehow related to both linters.

What was the problem with Record<string, unknown anyway ?

Copy link
Contributor Author

@tonyhallett tonyhallett May 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was the problem with Record<string, unknown anyway ?

When applied to a property typescript will complain

is not assignable to type 'Record<string | symbol, unknown>'. Index signature is missing in type CLASSNAME

This is why

class ClassWithDecorators {
  constructor(@decorator param:Dependency){}
  @decorator
  //@ts-ignore
  prop:Dependency

  @decorator
  static staticProp:Dependency

}
// here is the error
const typescriptError:Record<string,unknown> = ClassWithDecorators.prototype

the js

var ClassWithDecorators = /** @class */ (function () {
    function ClassWithDecorators(param) {
    }
    __decorate([
        decorator,
        __metadata("design:type", Dependency)
    ], ClassWithDecorators.prototype, "prop", void 0);
    __decorate([
        decorator,
        __metadata("design:type", Dependency)
    ], ClassWithDecorators, "staticProp", void 0);
    ClassWithDecorators = __decorate([
        __param(0, decorator),
        __metadata("design:paramtypes", [Dependency])
    ], ClassWithDecorators);
    return ClassWithDecorators;
}());

Copy link
Contributor Author

@tonyhallett tonyhallett May 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PodaruDragos
tell eslint to ignore the object and the Function types where they are used. The errors is there for lazy typing and errors that arise because of that.
In this instance the type signatures are ok and definitely better than unknown and of couse any.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the class being decorated had
[key:string]:unknown
Then Record<string | symbol, unknown> | interfaces.Newable<unknown> will not error

@notaphplover notaphplover self-requested a review May 12, 2021 14:23
Copy link
Contributor Author

@tonyhallett tonyhallett left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see #1343 (comment).

src/annotation/decorator_utils.ts Show resolved Hide resolved
prototype:T
}

export type DecoratorTarget<T = Object> = ConstructorFunction<T> | T
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that this is the best way of typing the target.
We differentiate between constructor function and prototype to prevent static properties and the prototype property is the proper way of doing so.
_tagParameterOrProperty requires annotationTarget: Function ( this cannot be typed better with generics ) and when target is a constructor function it is obviously so. Although it is through a cast in tagParameter _tagParameterOrProperty(METADATA_KEY.TAGGED, annotationTarget as ConstructorFunction, parameterIndex.toString(), metadata); the type predicate could have been used instead.


interface ConstructorFunction<T = Object>{
new (...args:unknown[]): T,
prototype:T
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think prototype should not be T since T is not an instance of the type, but the prototype instead

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

var ClassWithDecorators = /** @class */ (function () {
    function ClassWithDecorators(param) {
    }
    __decorate([
        decorator,
        __metadata("design:type", Dependency)
    ], ClassWithDecorators.prototype, "prop", void 0);
    __decorate([
        decorator,
        __metadata("design:type", Dependency)
    ], ClassWithDecorators, "staticProp", void 0);
    ClassWithDecorators = __decorate([
        __param(0, decorator),
        __metadata("design:paramtypes", [Dependency])
    ], ClassWithDecorators);
    return ClassWithDecorators;
}());

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mapped T to Prototype<T>, properties now optional.

type Prototype<T> = {
    [Property in keyof T ]:
        T[Property] extends Function?
            T[Property] :
            T[Property] | undefined
} & {constructor:Function}

interface ConstructorFunction<T = Object>{
    new (...args:unknown[]): T,
    prototype:Prototype<T>
}

export type DecoratorTarget<T = Object> = ConstructorFunction<T> |  Prototype<T>

tonyhallett and others added 2 commits May 13, 2021 10:44
Copy link
Contributor

@PodaruDragos PodaruDragos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definitely better ( nice work). feel free to merge when able. LGTM.

@tonyhallett
Copy link
Contributor Author

@PodaruDragos
@notaphplover

Thanks for your contributions on this and other pull requests !

Add this to the changelog as exports the helper createTaggedDecorator ?

Are we doing merge commits or rebase and merge ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants