Typescript: Better design:type and reflection

Created on 2 Apr 2017  ยท  16Comments  ยท  Source: microsoft/TypeScript

I know there are lot of design problems, but I would like to discuss 3 problems of current reflection mechanism.

  • design:type is useless when there are circular references. Its unreliable at all. This is the most tricky problem, because user expects that design:type will work, however once his models become circular they simply stop working. Its totally unexpected by a user, and breaks his app and only think he can do is to redesign everything and most probably not use design:type at all.

One possible solution for this problem maybe to store "factory", instead of type in design:type, e.g. instead of __metadata("design:type", Category_1.Category) do __metadata("design:type-factory", () => Category_1.Category).

  • second problem is to have types when decorator is not set. For example I want to perform deserialization to real object instances. I would like to simply pass a model, and serializer/deserializer can simply read from model's reflection and determine what classes it should use to perform deserialization. But to have these classes I need to add some dummy decorator on each property to have reflection information about them.

I think there should be possibility to enable type exposition for all or specific models without having to apply decorator on each class property. (if all or specific models is not possible, then at least do design:type for each class property if class has at least one class-decorator.

  • third well-known problem is arrays for @ManyToMany() categories: Category[] storing __metadata("design:type", Array) is almost useful. Would be great to have "Category_1.Category" too, we can do that by saving a bit more complex object, or store in a separate "design:subtype" metadata.

I know there are many problems with reflection and how to handle multiple different cases.
If solving all problems at once with a good design is not possible yet, maybe typescript can at least solve those three problems I described.

Decorators Revisit Suggestion

Most helpful comment

Im aware of proposal change and hope new proposal will be established asap and implemented in typescript. And it will be better then current.

Im know some shortcomings about angular di, however I don't think they are because of decorators. Decorators is one of my favorite feature of any language supporting them because they can make really heavy operations under the hood and provide a facade by making the code extremely clean, simple and declarative - the most important criteria to have a clean and maintainable codebase.

All 16 comments

You seem to have a fair amount of investment in this scenario so will want to read https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals#non-goals before continuing.

@aluanhaddad I know typescript's non-goals, what exact non-goal are you referring right now?

  1. Add or rely on run-time type information in programs, or emit different code based on the results of the type system. Instead, encourage programming patterns that do not require run-time metadata.

I should say that your second request does make a lot of sense. There is just a lot of discussion on the this topic already. There are a lot of requests for more runtime type information but TypeScript does not add runtime behavior to JavaScript.

If there are a lot of requests for this then it means it makes sense.

  1. Add or rely on run-time type information in programs...

You should treat this as something relative in this case. TypeScript already does it for decorators and reflect-metadata support, and I'm not talking about something new that should add run-time, I'm talking about something that already exists and how to improve it.

@pleerock
There is plenty of issues has related with emitting metadata.
I also wait correct output for that, a lot of time :)
I see only one way to fix that - write own transformer.

There is something interesting about struct emitting from @rbuckton
https://gist.github.com/rbuckton/f6ee6fcdcc21d44fdfa0

@pleerock TypeScript doesn't exactly emit member types and parameter types, it emits _references_ to the constructor functions having the names corresponding intuitively to said types. By default, it specified Object, Function, or undefined otherwise.
Array<T> --> Array
(x: string) => number --> Function
string | number --> Object
number | null --> Number
null -> undefined
undefined -> undefined
() => void --> Function when the type of a method
() => void --> Object when the type of a property

@aluanhaddad right and what Im asking in this issue is to _improve_ reference emit of the constructor function, not to allow typescript to emit "types for decorators".

@cevek yeah I waited for a long time too. Im using decorators from their beginning and know about annoying shortcomings of them. Finally decided to report something because if at least those problems will be solved, it will improve user experience of users who are using my open-source libraries much much more.

@pleerock You are correct. My words were poorly chosen I know you were not asking for serialization of decorator types themselves but I was not clear.

Anyway, I am sorry to come off as negative, but I think it is a bad idea to rely too heavily on decorators for several reasons.

For one, the proposal has changed. Currently TypeScript and Babel (but only via a legacy qualified plugin) both implement somewhat different versions of the old decorators proposal. Not an old version, but one which was scrapped and re-written

[rant]
For another, the reliance on serialized type info, which only works with classes (non-parametric), suggests first and foremost a coupling between interface and implementation. It is difficult to impossible to practice _programming to abstractions_ when using classes in ECMAScript as your API.

In a decorator emit driven world, like the Angular universe, decorators guide you down a painful path where single inheritance and erased interfaces will feel like limitations and likely lead to unhealthy coupling.

Obviously, this is just my opinion, but I think it sounds awful.

Angular 2's APIs are a great example of poor, unergonomic decorator use where many empty classes exist declared by users simply to be decorated so metadata can be assigned to arbitrary fields in what is effectively a global registration API.

Just let me export an object literal๐Ÿ˜.

I used to think the looseness of AngularJS (ng 1) string based DI abstraction was a shortcoming, after using it for more than 3 years, I don't think it was problematic at all.
[/rant]

Im aware of proposal change and hope new proposal will be established asap and implemented in typescript. And it will be better then current.

Im know some shortcomings about angular di, however I don't think they are because of decorators. Decorators is one of my favorite feature of any language supporting them because they can make really heavy operations under the hood and provide a facade by making the code extremely clean, simple and declarative - the most important criteria to have a clean and maintainable codebase.

Im aware of proposal change and hope new proposal will be established asap and implemented in typescript. And it will be better then current.

That's good. I would wait for them to stabilize but I kind of got stuck on two different points that are actually orthogonal.

  1. Use of decorators
  2. Use of "type" metadata

I think decorators have a lot of valuable use cases for adding runtime behavior and instrumentation but it is unfortunate that they are tied to ECMAScript classes. I do not like ECMAScript classes.

I think use of decorators to emit references to constructor functions is questionable.

the most important criteria to have a clean and maintainable codebase

You will get no argument from me (naturally).

As for the DI use of decorators in Angular it is actually not a big problem, it is just rather a bit awkward.
Interestingly the decorators that Angular uses are purely about creating metadata but they do not inject validation of the arguments passed in which they could, but I am glad they do not because it lets me write things like

import {Inject, ElementRef as NastyElementRef} from '@angular/core'

interface ElementRef {
    nativeElement: HtmlElement; // Work around intentionally bad API.
}

@Component(.....) export class SomeComponent {
  constructor(@Inject(NastyElementRef) readonly elementRef: ElementRef) {  }
}

Honestly my issue with Angular decorators has to do with their use of decorators for just about everything excepting DI and Input/Output which make some sense.
Also, they are pushing an ahead of time compilation mode (AOT) based on erasing (they call it lowering) decorators by statically transforming the compile time relevant submit of metadata into JSON and by doing so have broken compatibility with both TypeScript and ECMAScript in terms of the semantics of decorators. Of course in standard non AOT mode everything is normal but they break compat with just about everything. (this paragraph has been off topic)

In other words their usage is non-standard, does not capitalize on decorators, and actually leverages the fact that most programmers are not familiar with them to add an air of framework specific mystique.

They are just using them as a syntactic crutch at this point to say:
"Hey! Look! It is special framework only thing that is magical!"

This is not good for clean maintainable client code.

Their APIs are terrible.

That does not mean decorators are bad. I've seen them tastefully (moderately) used in other frameworks to good effect.

Perhaps I am letting my distaste for the Angular APIs color my conception of decorators which are actually very useful in some contexts that they fail to take full advantage of.

Having said that I would be wary of metadata emit and focus on the subset of TS decorator behavior that will be available in ES.

Angular internal problems aren't related to decorators and how they should work in TS. I respect a new decorators proposal (who won't, right? Even TS will forced do to that), however I think metadata emit is important to be performed by TS.

Angular internal problems aren't related to decorators and how they should work in TS.

Indeed, I went off topic.

I respect a new decorators proposal (who won't, right? Even TS will forced do to that)

I don't think anyone has to force TypeScript's hand in being standards compliant. The language emphasizes it to the letter.

I do think that Angular may fight against upgrading and try to hold hundreds of thousands of develops back. They have been on an outdated version of TypeScript for like 5 months.

however I think metadata emit is important to be performed by TS.

I respectfully disagree.

Im aware of proposal change and hope new proposal will be established asap and implemented in typescript. And it will be better then current.

We are working with TC39 on the proposal. once the proposal reaches a solid state we will be implementing it and sharing a plan for migration for our users.

When that proposal lands we intend to revamp the metadata emit to use functions instead of values, this will solve the circularity issue.

that said, we have no plans to expand the metadata support, nor to serialize structural types. we believe this still outside the scope of the TS project in the time being.

@aluanhaddad

number | null --> Number

This doesn't seem to be the case anymore โ€“ it returns Object now. Is that correct behaviour or a bug?

@mmiszy That intuitively seems wrong.

I also noticed that the behavior of | undefined is really strange here.

function d(_: object, __: string) {}

class A {
  @d p: number | undefined;
}

results in

__decorate([
    d,
    __metadata("design:type", Object)
], A.prototype, "p", void 0);

But

function d(_: object, __: string) {}

class A {
  @d p?: number;
}

results in

__decorate([
    d,
    __metadata("design:type", Number)
], A.prototype, "p", void 0);

Maybe it'd be useful for someone:

To avoid reference error on circular dependencies while keeping type information in your SDK you can do something like

class {
  @Field
  circularField: CircularType | undefined; // add `| undefined` so 'design:type' becomes Object
}

Most propably, you'd have to manually pass type getter function to decorator options (and it's pain in the a**), but it'll not break your app.

I'm really waiting to have __metadata as factory. I was trying to create PR but it overwhelmed me a bit.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

MartynasZilinskas picture MartynasZilinskas  ยท  3Comments

bgrieder picture bgrieder  ยท  3Comments

Roam-Cooper picture Roam-Cooper  ยท  3Comments

remojansen picture remojansen  ยท  3Comments

Antony-Jones picture Antony-Jones  ยท  3Comments