microsoft / microsoft/TypeScript

Analysis support for metaprogramming decorators

Offen
#26,347 2 Kommentare 13 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Dieses Issue hat noch niemand übernommen.

Needs Proposal Suggestion
Vorherrschende Sprache
Go
Sterne
111k
Forks
14.4k
Ø Merge
1 T. 19 Std.
Gemergte PRs (30 T.)
117

Beschreibung

Search Terms

decorator, metaprogramming, rename, renaming

Suggestion

As decorators near Stage 3, use cases are coming up that would currently pose problems for TypeScript due to their dynamic nature. Any additional properties added won't be visible, renamed properties won't analyze, and custom visibility schemes like protected or friends won't be understood.

This is another area where the dynamic nature of JavaScript might stretch the TypeScript type system, but with mapped and conditional types we might be able to describe a subset of the use-cases in the type system.

I don't have a concrete solution to suggest, but a couple of requirements that a solution should meet:

  • Describe the change in type of a decorated property
  • Describe the change in name of a property
  • Describe newly introduced properties (ie, private storage for an @observed property)
  • Describe changes to a class (ie, added static properties)

Mapped and conditional types already support some type transformation, but not transforming the keys, or describing a transformation across the static and instance types.

Maybe there's a way to expand on the list-comprehension-like syntax for mapped types to be even more like a list comprehension and allow

  • Transformations of property names
  • Filtering

Here's a rough idea of a mapping that would transform a key name to a symbol, and filter by properties decorated with a specific decorator:

type Namespaced<T> = {
  [Symbol(P): T[P] for P in keyof T where namespaced decorates P];
}

Symbol(P) and where namespaced decorates P are obviously very made up and probably not great choices choices.

That's not exactly right for use from individual decorators though. Since decorators take and return an extended PropertyDescriptor, maybe we can just type the decorator itself:

const namespaced = <P extends PropertyDescriptor>(descriptor: P): NamespacedPropertyDescriptor<P> => { ... }

type NamespacedPropertyDescriptor<P  extends PropertyDescriptor> = {
  key: Symbol(P['key']);
} extends P;

(I threw in the extends type operator because key must override key in P, so an intersection won't work. Some kind of object-spread-like syntax could also work)

That describes that the key of the property is transformed, but it doesn't associate it with the declaration of that symbol.

One possibility for that is for TypeScript to understand the type of the finisher method on the property descriptor and use that to infer changes to the class itself:

// P is the PropertyDescriptor passed to the decorator
// C is the constructor type for static properties, there would need to be a instance type
// as well
type NamespacedPropertyDescriptor<P  extends PropertyDescriptor, C> = {
  key: typeof C[P['key']];
  finisher(klass: C): NamespacedClass<C, P['key']>;
};

type NamespacedClass<C, K> = {
  readonly K: symbol;
} extends C;

I realize I just put a lot of hand-wavy, complicated, probably not workable ideas up there, but maybe there's some better direction for the solution (short of adding an imperative type language) those could inspire.

Another approach would be for TypeScript to intrinsically understand the operation of a few well-known decorators as and not allow their definition within the type system itself.

Use Cases

One example of name-rewriting decorators is to prevent name collisions with string properties by renaming a property to a unique Symbol defined on the class:

abstract class A {
  @namespaced f() {}
}

abstract class B {
  @namespaced f() {}
}

class C implements A, B {
  [A.f]() {
    console.log('C[A.f]');
  }
  [B.f]() {
    console.log('C[B.f]');
  }
}

Class A above would be equivalent to this manual namespacing:

class A {
  static f = Symbol();
  [A.f]() {}
}

Examples

/**
 * Rewrites a string-keyed class property to use a Symbol defined on the constructor.
 * @
 */
const namespaced = (descriptor: PropertyDescriptor) => {
  if (typeof descriptor.key === 'symbol') {
    return descriptor;
  }
  const symbol = Symbol(descriptor.key);
  return {
    key: symbol,
    ...descriptor,
    finisher(klass) { Object.defineProperty(klass, descriptor.key, {value: symbol}); }
  };
};

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript / JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. new expression-level syntax)

Beitragsleitfaden

Beitragsleitfaden öffnen

Erste Schritte

  1. Lies das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreib ins Issue, dass du es übernimmst — das erspart doppelte Arbeit.
  3. Forke das Repository und arbeite in einem Branch.
  4. Öffne einen Pull Request, der die Issue-Nummer nennt.

Rechercherichtung

Beginne mit der Überprüfung der Decorator-Beispiele und der Anforderungen für das Ändern von Eigenschaftstypen und -namen, das Hinzufügen von Eigenschaften und das Transformieren von Klassentypen. Es werden keine Dateien, Tests oder Implementierungseinstiegspunkte genannt, und der Vorschlag enthält keine konkrete Lösung. Für den Abschluss wären ein definiertes Type-System-Design sowie die entsprechende Implementierung und Tests erforderlich.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Bewertung

Tech-Stack
javascript, typescript
Bereich
compilers
Issue-Typ
Feature
Schwierigkeit
5/5
Geschätzter Aufwand
Über eine Woche
Aktivitätsstatus
Veraltet
Klarheit
Muss geklärt werden
Anfängerfreundlichkeit
20/100

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.