TypeScript Proposal: decoratedWith type operator and generic 'value' parameters

sf6xfgos  于 6个月前  发布在  TypeScript
关注(0)|答案(6)|浏览(49)

🔍 Search Terms

Decorators
TypeScript 5

✅ Viability Checklist

  • 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. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals

⭐ Suggestion

  • WIP: Goal is to get this to canonical ASAP, all help appreciated.*

This is a proposal introduces two things a type operator named: decoratoredWith and generic 'value' parameters.
First off, the decoratedWith type operator.
This type operator would check if a method, property/field, getter, setter, auto-accessor or class is decorated with a specific decorator function. (I think it shouldn't support legacy decorators)
To illustrate:

// Decorator functions
function EntityOnly<C, T>(_target: undefined, context: ClassFieldDecoratorContext<C, T>) {
  // Does something
}

function SchemaProperty<C, T>(schema: any) {
  return function(_target: undefined, context: ClassFieldDecoratorContext<C, T>) {
    // Does something
  }
}

// Class with some fields decorated
class User {
  id: number;
  @SchemaProperty({ type: string, })
  username: string;
  @SchemaProperty({ type: string, })
  displayName: string;
  @EntityOnly
  hashedPasssword: any;
  @EntityOnly
  IBAN: string;
}

// A simple example of the decoratedWith type operator albeit this is not where this operator would shine.
type IsEntityOnly = User['hashedPassword'] decoratedWith EntityOnly ? true : false;

// Should also work with functions returning decorators.
type IsEntityOnly = User['username'] decoratedWith SchemaProperty ? true : false;

Should work the same with methods, properties/fields, getters, setters, auto-accessors and classes too.

Revision 1: Generic 'value' parameters

Disclaimer: I understand that the following might be such a substantial addition to the proposal that it should be a separate proposal but I am looking for feedback first before adding yet another proposal. Perhaps this part is already covered in another proposal, if so, great, if not then perhaps there is a reason this hasn't been proposed yet.
My initial proposal featured some code using the decoratedWith proposal with a generic type which wasn't really a type but rather a value pointing to a decorator function. (as pointed out by @jcalz) And although this is currently not possible in TypeScript I think it could be very beneficial to be able to generalize decorator functions as values. Perhaps these "generic 'value' parameters should be supported by their own group of 'value' operators which would refer to a value rather than a type. These values must be constant in nature, so would be limited to functions, const variables, enums, classes, etc. Naturally they cannot refer to types. Here's the list of 'value' operators:

  • valueOf - The one 'value' operator to rule them all. This 'value' operator accepts any value constant in nature.
  • funcOf - This 'value' operator accepts functions.
  • constOf - This 'value' operator refers to const variables.
  • enumOf - This 'value' operator refers to enums.
  • classOf - This 'value' operator refers to classes.

These new operators could streamline some processes like referring to an actual class rather than just it's constructor.
Let me start by illustrating some problems that generic 'value' operators could solve on their own:
The first problem would be that you can't enforce (at least not in a simple way as far as my knowledge goes, any examples that contradict this are appreciated as I could use that to enhance my proposal) a generic type parameter to be a class. The problem illustrated:

type ClassType<T> = new () => T;

// This user class takes a generic type parameter.
// The constructor has a parameter which requires a value with a constructor returning type T.
// Yet you cannot demand the generic parameter to be a class.
class User<T> {
  private _classType: ClassType<T>;
  constructor(classType: ClassType<T>) {
    this._classType = classType;
  }
}

interface Entity {

}
class Item implements Entity {
}
// As shown here, generic parameter T can be an interface while you might to enforce the type being a class.
new User<Entity>(Item);

What my proposal would do is, it would enable users to do the following:

class User<classOf T> {
  private _classType = T;
}

interface Entity {
}
class Item extends Entity {
}
// This shouldn't work because Entity is not a class.
new User<Entity>();
// This should work because Item is a class.
new User<Item>();

Here the 'value' operator classOf is used to denote a generic 'value' parameter (limited to class values). Generic 'value' parameters accept values rather than types and can be used throughout a class or function as value. The User class as described in the problem would translate to a class similar as that of the User class described in the previously illustrated problem which the classOf operator should solve. The only difference being that the classOf operator shortens the code and could restrict the generic parameter to actual classes ,
The other 'value' operators operate the same way where valueOf is the swiss army knife of the bunch which accepts every value accepted by all the other 'value' operators.
Generic 'value' parameters powered by 'value' operators can enable you to use constant values in type declarations.
To take my proposed decoratedWith for example:

// Together with the 'value' operators and generic 'value' parameters this is where `decoratedWith` could truly shine.
// Here it only returns the key names of the members of generic parameter T that are decorated with the decorator function provided in the generic 'value' parameter DT which must be a function.
type DecoratedMembers<T, funcOf DT> = keyof { [P in keyof T as T[P] decoratedWith DT ? P : never]: any; };

// This variable would have the union type: "hashedPassword" | "IBAN"
const entityOnlyMembers: DecoratedMembers<User, EntityOnly>;

// This variable would have the union type: "username" | "displayName"
const entityOnlyMembers: DecoratedMembers<User, SchemaProperty>;

This is all theoretically possible within TypeScript without requiring any change in the JavaScript spec. I am open for feedback. Also I am no longer sure if this is indeed not a breaking change (as it adds keywords), so if anyone could comment on that, that be great too.

📃 Motivating Example

You can now work with types that are computed based on the fact whether methods, properties/fields, getters, setters, auto-accessors or classes include certain decorators or not.

💻 Use Cases

  1. What do you want to use this for?
    To be able to act on the inclusion or exclusion of decorators on methods, properties/fields, getters, setters auto-accessors and classes.
  2. What shortcomings exist with current approaches?
    There aren't really ways to handle such cases right now.
  3. What workarounds are you using in the meantime?
    There aren't really any workarounds right now as far as my knowledge goes.
vql8enpb

vql8enpb1#

你的提案中的代码对我来说有些含糊不清。如果你想区分两种相同类型的不同装饰器,decoratedWith 后面的部分必须是值,而不是类型。但是TS没有提供这样的操作符抽象功能。你不能写 type F<T> = ⋯ decoratedWith T ⋯ ,因为 T 是一个泛型类型,而不是一个值。同样的原因是你不能像 type TypeOf<T> = typeof T 那样为 typeof 创建自定义别名。当你定义 type DecoratedMembers<T, DT> = ⋯ decoratedWith DT ⋯ 时,以及提到 DecoratedMembers<User, EntityOnly> 时,你在混淆类型和值,因为 EntityOnly 不是一个类型的名称。如果这是一个关于新的 decoratedWith 操作符的提案,而不是允许抽象值接受类型操作符的附加提案,那么你可能需要编辑以删除这部分内容,只使用 decoratedWith 内联。

vsmadaxz

vsmadaxz2#

@jcalz 我明白你的意思了,Joe,你是对的,EntityOnly不是一个类型,而是一个值。请问在谈论抽象化接受值类型的操作符时,第一个想到的提案是什么?因为这可能会帮助我微调这个提案,因为虽然内联变体可能是有用的,但它不是我在这里的重点。

afdcj2ne

afdcj2ne3#

我不知道是否存在现有的提案。鉴于内联decoratedWith操作符是这里其他内容的先决条件,我很惊讶它不是你的关注点。

qc6wkl3g

qc6wkl3g4#

@jcalz,这确实是关于decoratedWith操作符的问题,但我的意思是,如果能将其右侧部分进行泛化,它可能会真正发光。但这里的难点在于,你需要将函数作为值而不是类型来引用。我猜这就是你的意思,因此我问你是否知道任何已知的提案,可以实现类似的功能。我相信这可以在TypeScript生态系统内完成,而无需对JavaScript规范进行任何更改,只是试图找到一种在TypeScript所需的更改相对简单且不苛刻的情况下实现这一点的好方法。我希望这能给你一个大致了解我在说什么的概念。

xqk2d5yq

xqk2d5yq5#

@jcalz 我对你最初的反馈进行了修改。现在它包括了关于在类型中使用常量值的提案,如果你不介意的话,我想听听你对此的看法。

zu0ti5jz

zu0ti5jz6#

坦率地说,我不太清楚该如何处理这个问题。范围一直在不断扩大,我并不一定想要深入审查任何例子,尤其是如果结果会导致范围再次扩大的话。例如,类型系统并不能表示类示例类型和任意接口之间的差异,因此任何试图隐式暴露这种功能的例子都要求进行更改。我宁愿保持距离而不是承诺进一步遵循这个方向。祝你好运!

相关问题