From a8cdafc8c4d546fd18d3a71728f2bfbb18fe90c8 Mon Sep 17 00:00:00 2001 From: Nicholas Berlette Date: Sun, 26 May 2024 22:37:42 -0700 Subject: [PATCH 01/11] feat(types): add stage3 decorator types --- packages/types/accessor.ts | 111 +++++++++++++++++++++++++++++++++++++ packages/types/class.ts | 72 ++++++++++++++++++++++++ packages/types/field.ts | 95 +++++++++++++++++++++++++++++++ packages/types/getter.ts | 98 ++++++++++++++++++++++++++++++++ packages/types/method.ts | 106 +++++++++++++++++++++++++++++++++++ packages/types/setter.ts | 97 ++++++++++++++++++++++++++++++++ 6 files changed, 579 insertions(+) create mode 100644 packages/types/accessor.ts create mode 100644 packages/types/class.ts create mode 100644 packages/types/field.ts create mode 100644 packages/types/getter.ts create mode 100644 packages/types/method.ts create mode 100644 packages/types/setter.ts diff --git a/packages/types/accessor.ts b/packages/types/accessor.ts new file mode 100644 index 0000000..1daaf3a --- /dev/null +++ b/packages/types/accessor.ts @@ -0,0 +1,111 @@ +/** + * @module accessor + * + * This module provides types for working with class auto-accessors according + * to the latest version of the TC39 Decorators Proposal (now at Stage 3). + * + * The types in this module provide a structured and well-documented + * foundation for creating consistent auto-accessor decorators with easily + * predictable behavior. Auto-accessors are a new type of class member + * introduced in the latest version of the Decorators Proposal, and introduce + * a new `accessor` keyword, which scaffolds a `getter`/`setter` pair backed + * by a private field to store the value. + * + * @example + * ```ts + * import type { ClassAccessorDecoratorFunction } from "@decorators/types/accessor"; + * + * const log = (prefix: string): ClassAccessorDecoratorFunction => + * (_, context) => { + * console.log(`[${prefix}] Initializing ${String(context.name)}`); + * }; + * + * class Example { + * @log("Example") accessor field = "value"; + * } + * ``` + * @category Auto-Accessor Decorators + */ + +/** + * Represents the decorator signature applied to a class auto-accessor member. + * + * #### What are auto-accessors? + * + * The latest form of the Decorators Proposal introduces a brand new type of + * class member and an entirely new `"accessor"` keyword. These members are + * known as "auto-accessors", and their decorators are "auto-accessor + * decorators". + * + * These behave just like a regular class field at runtime, but are backed by + * a private field to store the value. This allows the decorator to access the + * field's value at runtime, and also to replace the field's value with a new + * value. + * + * The purpose of this new language feature is to provide a remedy for the + * shortcomings of field decorators, which are explained in the comments on + * the {@linkcode ClassFieldDecoratorFunction} type. Auto-accessors seek to provide an + * alternative solution that offers the functionality that field decorators + * lack, while behaving **_close_** to how a standard field behaves at + * runtime. + * + * It's important to note that **these are not fields**. They are auto-created + * getters / setters that are backed by a private field for storage. + * + * ### Target Argument + * + * This is the only type of decorator that receives an object for its target + * argument, which is a special descriptor-style object containing the + * original `get` and `set` methods of the auto-accessor. + * + * ### Context Argument + * + * The `context` argument is a {@linkcode ClassAccessorDecoratorContext} + * object containing the metadata and other information about the + * auto-accessor being decorated. It's `kind` property will always be + * `"accessor"`. + * + * Similar to the other decorator types, it also includes an `addInitializer` + * method, `name` and `metadata` properties. And like all class member + * decorators, it also has `access`, `static`, and `private` properties. + * + * Since the `target` argument has a `get` and a `set` property, you can + * expect to find a `has`, `get`, and `set` method on the `context.access` + * object. + * + * ### Return Value + * + * The return value of an auto-accessor decorator may either be `void` (if no + * changes are needed), or a {@linkcode ClassAccessorDecoratorResult} object. + * This is an object similar to the target object, with `get` and/or `set` + * methods, and also optionally an `init` method. + * + * These optional methods are used to replace the original `get` or `set` + * methods, or to provide a custom initialization function for the + * auto-accessor's private backing field, respectively. The `init` method + * shares the same signature as {@linkcode ClassFieldDecoratorResult}. + * + * @template This The type on which the class element will be defined. For a + * static class element, this is the type of the constructor. For non-static + * class elements, this will be the type of the instance. + * @template Value The type of the class auto-accessor's value. + * @category Auto-Accessor Decorators + */ +export interface ClassAccessorDecoratorFunction< + This = unknown, + Value = unknown, + Return extends void | ClassAccessorDecoratorResult = + | void + | ClassAccessorDecoratorResult, +> { + /** + * @param target The descriptor-style object containing the original `get` + * and `set` methods of the auto-accessor. + * @param context The context object for the auto-accessor decorator. + * @returns Either `void` or a {@link ClassAccessorDecoratorResult} object. + */ + ( + target: ClassAccessorDecoratorTarget, + context: ClassAccessorDecoratorContext, + ): Return; +} diff --git a/packages/types/class.ts b/packages/types/class.ts new file mode 100644 index 0000000..eb1e59c --- /dev/null +++ b/packages/types/class.ts @@ -0,0 +1,72 @@ +/** + * @module class + * + * This module provides types for working with class decorators according to + * the latest version of the TC39 Decorators Proposal (now at Stage 3). + * + * The types in this module provide a structured and well-documented foundation + * for creating consistent class decorators with easily predictable behavior. + * + * @example + * ```ts + * import type { ClassDecoratorFunction } from "@decorators/types/class"; + * + * const log = (prefix: string): ClassDecoratorFunction => (target, context) => { + * console.log(`[${prefix}] Initializing ${String(context.name)}`); + * }; + * + * @log("Example") + * class Example { + * // ... + * } + * ``` + * @category Class Decorators + */ +import type { Constructor } from "./_internal.ts"; + +/** + * Represents a class decorator function's signature. + * + * #### Target Argument + * + * The `target` argument is the class constructor itself. This is the only + * type of decorator that receives a constructor as its first argument. + * + * #### Context Argument + * + * The `context` argument is a {@linkcode ClassDecoratorContext} object, which + * contains the metadata and other information about the class being + * decorated. The `kind` property of this object will always be `"class"`. + * The `name` property will be the name of the class being decorated, or + * `undefined` if it is an anonymous class / class expression. The + * `addInitializer` method (found in all decorator context objects) allows the + * registration of callback functions that will then be executed at the time + * of initialization. The `this` context binding will be the class constructor + * in this instance. + * + * It also has a `metadata` property, which is shared across all decorators + * applied to the same class, and goes on to become the `[Symbol.metadata]` + * property of the class constructor once all decorators have been applied. + * + * @template {AbstractClass} Class The type of the class + * constructor. This is the type of the {@link target} argument, and will be + * the type of the contextual `this` binding for any hooks registered with the + * {@linkcode addInitializer} method, as well as any static class members. + * @template {object} [Proto=object] The type of the class prototype. This is + * the type of the contextual `this` binding for any instance class members, + * as well as the body of the class itself and its constructor function. + * @template {void | Class} [Return=void|Class] The return type of the class + * decorator function. This may be `void` if the decorator does not need to + * return a replacement constructor; otherwise it **must** return the same type + * of constructor as the original class it is decorating (or a subclass + * thereof). No additional properties may be added by a decorator, unless they + * were already present on the original class prior to decoration. + * @category Class Decorators + */ +export interface ClassDecoratorFunction< + Proto extends object = object, + Class extends Constructor = Constructor, + Return extends void | Class = void | Class, +> { + (target: Class, context: ClassDecoratorContext): Return; +} diff --git a/packages/types/field.ts b/packages/types/field.ts new file mode 100644 index 0000000..0e46e9a --- /dev/null +++ b/packages/types/field.ts @@ -0,0 +1,95 @@ +/** + * @module field + * + * This module provides types for working with class field decorators + * according to the latest version of the TC39 Decorators Proposal (now at + * Stage 3). + * + * The types in this module provide a structured and well-documented + * foundation for creating consistent field decorators with easily predictable + * behavior. + * + * @example + * ```ts + * import type { ClassFieldDecoratorFunction } from "@decorators/types/field"; + * + * const log = (prefix: string): ClassFieldDecoratorFunction => (_, context) => (initialValue) => { + * console.log( + * `[${prefix}] Initializing ${String(context.name)} with ${initialValue}`, + * ); + * return initialValue; + * }; + * + * class Example { + * @log("Example") static staticField = "static"; + * } + * ``` + * @category Field Decorators + */ + +/** + * Represents the decorator signature applied to a class field. This is the + * only decorator type that will always have `undefined` as its first + * argument, and receives a {@linkcode ClassFieldDecoratorContext} object for + * its second argument. + * + * Field decorators are able to return a custom initializer function, which + * can be used to mutate or replace the original initial value of the field. + * See the {@linkcode ClassFieldDecoratorResult} type for more information. + * + * @template This The type on which the class element will be defined. For a + * static class element, this is the type of the constructor. For non-static + * class elements, this will be the type of the instance. + * @template Value The type of the class field's value. + * @category Field Decorators + */ +export interface ClassFieldDecoratorFunction< + This = unknown, + Value = unknown, + Return extends void | ClassFieldDecoratorResult = + | void + | ClassFieldDecoratorResult, +> { + /** + * @param target Always `undefined`. + * @param context The context object for the class field decorator. + * @returns Either `void` or a custom {@link ClassFieldDecoratorResult} to + * initializer the field with a different value. + */ + ( + target: undefined, + context: ClassFieldDecoratorContext, + ): Return; +} + +/** + * Represents the result of a class field decorator function. This is a custom + * initializer function that can be used to modify or replace the initial + * value of the field being decorated. + * + * Inside these custom initializer functions is **the only point** that + * initial values of class field are accessible to the decorator, due to the + * nature of the initialization process of classes fields by the ECMAScript + * language. This is why the `target` parameter of field decorators is always + * `undefined` - the field's value is not yet initialized when its decorators + * are applied. + * + * If you'd like to access the value of a field before it's initialized, you + * would probably benefit from using an auto-accessor instead of a classic + * field. See the {@linkcode ClassAccessorDecoratorFunction} type for more information + * on auto-accessors and how to use them. + * + * @template This The type on which the class element will be defined. For a + * static class element, this is the type of the constructor. For non-static + * class elements, this will be the type of the instance. + * @template Value The type of the class field's value. + * @category Field Decorators + */ +export interface ClassFieldDecoratorResult { + /** + * @this This The instance/constructor of the class being decorated. + * @param initialValue The initial value of the field being decorated. + * @returns The new value to be used as the initial field value. + */ + (this: This, initialValue: Value): Value; +} diff --git a/packages/types/getter.ts b/packages/types/getter.ts new file mode 100644 index 0000000..d090c9b --- /dev/null +++ b/packages/types/getter.ts @@ -0,0 +1,98 @@ +/** + * @module getter + * + * This module provides types for working with class getter decorators + * according to the latest version of the TC39 Decorators Proposal (now at + * Stage 3). + * + * The types in this module provide a structured and well-documented + * foundation for creating consistent getter decorators with easily + * predictable behavior. + * + * @example + * ```ts + * import type { ClassGetterDecoratorFunction } from "@decorators/types/getter"; + * + * const log = (): ClassGetterDecoratorFunction => + * (target, context) => { + * return function () { + * const value = target.call(this); + * console.log(`Getting ${String(context.name)} ... ${value}`); + * return value; + * }; + * }; + * + * class Example { + * #field = "value"; + * + * @log() get field() { return this.#field; + * } + * } + * + * const example = new Example(); + * + * example.field; // logs "Getting field ... value" + * ``` + */ + +/** + * Represents a getter method on a class that is being decorated, regardless + * of whether its static/instance and public/private. + * + * @template [This=unknown] The type on which the class element will be + * defined. For a static class element, this is the type of the constructor. + * For non-static class elements, this will be the type of the instance. + * @template [Value=unknown] The type of the getter's return value. + * @category Getter Decorators + */ +export interface ClassGetter { + (this: This): Value; +} + +/** + * Represents the decorator signature applied to a class getter member. + * + * #### What are getters? + * + * A getter is a special type of method that is used to read the value of a + * property. It is defined using the `get` keyword, followed by an identifier + * and a block of code that returns the value of the property. + * + * ### Target Argument + * + * The `target` argument is the getter method itself, and is a function that + * receives no arguments and returns the value of the property. + * + * ### Context Argument + * + * The `context` argument is a {@linkcode ClassGetterDecoratorContext} object + * containing the metadata and other information about the getter being + * decorated. It's `kind` property will always be `"getter"`. + * + * Similar to the other decorator types, it also includes an `addInitializer` + * method, `name` and `metadata` properties. And like all class member + * decorators, it also has `access`, `static`, and `private` properties. + * + * ### Return Value + * + * The return value of a getter decorator may either be `void` (if no changes + * are needed), or a new getter function to replace the original getter. + * + * @template This The type on which the class element will be defined. For a + * static class element, this is the type of the constructor. For non-static + * class elements, this will be the type of the instance. + * @template Value The type of the class getter. + * @category Getter Decorators + */ +export interface ClassGetterDecoratorFunction< + This = unknown, + Value = unknown, + Return extends void | ClassGetter = + | void + | ClassGetter, +> { + ( + target: ClassGetter, + context: ClassGetterDecoratorContext, + ): Return; +} diff --git a/packages/types/method.ts b/packages/types/method.ts new file mode 100644 index 0000000..586eac8 --- /dev/null +++ b/packages/types/method.ts @@ -0,0 +1,106 @@ +/** + * @module method + * + * This module provides types for working with method decorators according to + * the latest version of the TC39 Decorators Proposal (now at Stage 3). + * + * The types in this module provide a structured and well-documented + * foundation for creating consistent method decorators with easily + * predictable behavior. + * + * @example + * ```ts + * import type { ClassMethod, ClassMethodDecoratorFunction } from "@decorators/types/method"; + * + * const bind = >(): ClassMethodDecoratorFunction => + * (target, context) => { + * context.addInitializer(function() { + * this[context.name] = this[context.name].bind(this); + * }); + * }; + * + * class Example { + * #field = "value"; + * + * @bind() method() { return this.#field; + * } + * } + * + * const example = new Example(); const method = example.method; + * + * console.log(method()); // logs "value" + * ``` + * @category Method Decorators + */ + +/** + * Represents a method on a class that is being decorated, whether its static + * or an instance method, public or private. + * + * @template [This=unknown] The type on which the class element will be + * defined. For a static class element, this is the type of the constructor. + * For non-static class elements, this will be the type of the instance. + * @template [Args=any[]] The type of the method's arguments. + * @template [Return=unknown] The type of the method's return value. + * @category Method Decorators + */ +export interface ClassMethod< + This = unknown, + // deno-lint-ignore no-explicit-any + Args extends readonly unknown[] = any[], + Return = unknown, +> { + (this: This, ...args: Args): Return; +} + +/** + * Represents the decorator signature applied to a class method. + * + * ### Target Argument + * + * The `target` argument is the method itself, and is a function that receives + * the same arguments as the method, and returns the same value as the method. + * + * ### Context Argument + * + * The `context` argument is a {@linkcode ClassMethodDecoratorContext} object + * containing the metadata and other information about the method being + * decorated. It's `kind` property will always be `"method"`. + * + * Similar to the other decorator types, it also includes an `addInitializer` + * method, `name` and `metadata` properties. And like all class member + * decorators, it also has `access`, `static`, and `private` properties. + * + * ### Return Value + * + * The return value of a method decorator may either be `void` (if no changes + * are needed), or a new function to replace the one being decorated. + * + * @template [This=unknown] The type on which the class element will be + * defined. For a static class element, this is the type of the constructor. + * For non-static class elements, this will be the type of the instance. + * @template [Value=ClassMethod] The type of the class method. + * @template [Return=void|Value] The return type of the method decorator. For + * decorators that return a new, replacement method, the return type will be + * the same as the method being decorated (since they must maintain the same + * signature). For decorators that do not return a value, this will be `void`. + * Defaults to `void | Value`, meaning both of those scenarios are allowed. + * @category Method Decorators + */ +export interface ClassMethodDecoratorFunction< + This = unknown, + Value extends ClassMethod = ClassMethod, + Return extends void | Value = void | Value, +> { + /** + * @param target The method itself. + * @param context The context object for the method decorator. + * @returns Either `void` or a new method function to replace the original. + * The type of the returned value is controlled by the `Return` type param + * on the {@link ClassMethodDecoratorFunction} interface. + */ + ( + target: Value, + context: ClassMethodDecoratorContext, + ): Return; +} diff --git a/packages/types/setter.ts b/packages/types/setter.ts new file mode 100644 index 0000000..4d0ce85 --- /dev/null +++ b/packages/types/setter.ts @@ -0,0 +1,97 @@ +/** + * @module setter + * + * This module provides types for working with class setter decorators according + * to the latest version of the TC39 Decorators Proposal (now at Stage 3). + * + * The types in this module provide a structured and well-documented foundation + * for creating consistent setter decorators with easily predictable behavior. + * + * @example + * ```ts + * import type { ClassSetterDecoratorFunction } from "@decorators/types/setter"; + * + * const log = (): ClassSetterDecoratorFunction => + * (target, context) => { + * return function (newValue) { + * console.log(`Setting ${String(context.name)} to ${newValue}`); + * target.call(this, newValue); + * }; + * }; + * + * class Example { + * #field = "value"; + * + * @log() set field(newValue: string) { + * this.#field = newValue; + * } + * } + * + * const example = new Example(); + * + * example.field = "new value"; // logs "Setting field to new value" + * ``` + * @category Setter Decorators + */ + +/** + * Represents a setter method on a class that is being decorated, regardless + * of whether its static/instance and public/private. + * + * @template [This=unknown] The type on which the class element will be defined. For a + * static class element, this is the type of the constructor. For non-static + * class elements, this will be the type of the instance. + * @template [Value=unknown] The type of the setter's argument. + * @category Setter Decorators + */ +export interface ClassSetter { + (this: This, newValue: Value): void; +} + +/** + * Represents the decorator signature applied to a class setter member. + * + * #### What are setters? + * + * A setter is a special type of method that is used to write the value of a + * property. It is defined using the `set` keyword, followed by an identifier + * and a block of code that sets the value of the property. + * + * #### Param: `target` + * + * The `target` argument is the setter method itself, and is a function that + * receives the new value of the property as its only argument. + * + * #### Param: `context` + * + * The `context` argument is a {@linkcode ClassSetterDecoratorContext} object + * containing the metadata and other information about the setter being + * decorated. It's `kind` property will always be `"setter"`. + * + * Similar to the other decorator types, it also includes an `addInitializer` + * method, `name` and `metadata` properties. And like all class member + * decorators, it also has `access`, `static`, and `private` properties. + * + * #### Returned Value + * + * The return value of a setter decorator may either be `void` (if no changes + * are needed), or a new setter function to replace the original setter. + * + * @template This The type on which the class element will be defined. For a + * static class element, this is the type of the constructor. For non-static + * class elements, this will be the type of the instance. + * @template Value The type of the class setter's expected value. + * @category Setter Decorators + */ +export interface ClassSetterDecoratorFunction< + This = unknown, + Value = unknown, + Return extends void | ClassSetter = + | void + | ClassSetter, +> { + ( + target: ClassSetter, + context: ClassSetterDecoratorContext, + ): Return; +} From 46e5479b19d50c80955f77fbf3e7cf264c77287b Mon Sep 17 00:00:00 2001 From: Nicholas Berlette Date: Sun, 12 May 2024 15:38:14 -0700 Subject: [PATCH 02/11] chore(internal): update internal types --- internal/types.ts | 143 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 120 insertions(+), 23 deletions(-) diff --git a/internal/types.ts b/internal/types.ts index d6b14ea..f7eaf48 100644 --- a/internal/types.ts +++ b/internal/types.ts @@ -1,11 +1,16 @@ -export type As = T extends U ? U extends T ? U : T - : [T & U] extends [never] ? U & Omit - : T & U; - -export type Is = [T] extends [never] ? [U] extends [never] ? never : U - : T extends unknown ? U - : T extends U ? T - : U; +// deno-lint-ignore-file no-explicit-any +/** + * @module types + * + * This module provides internal utility types used throughout various projects + * in the `@decorators` namespace. These types are not intended for direct use + * by end users, but are instead used internally to provide type safety and + * consistency across the various projects and modules. They're located in this + * module to avoid redundancy and improve maintainability. + * + * @see https://jsr.io/@decorators for more information and a full list of the + * available packages offered by the `@decorators` project namespace. Thanks! + */ export const BRAND: unique symbol = Symbol("BRAND"); export type BRAND = typeof BRAND; @@ -13,7 +18,17 @@ export type BRAND = typeof BRAND; export const NEVER: unique symbol = Symbol("NEVER"); export type NEVER = typeof NEVER; -export interface Brand { +/** + * Represents a unique brand for a type, which can be used to create nominal + * types in TypeScript and prevent type collisions. For a less-strict version + * of this type, see the {@linkcode Flavor} interface. + * + * To create a branded type, you can either use the {@linkcode Branded} helper + * type, or manually extend/intersect another type with this interface. The + * {@linkcode A} type parameter is the type that becomes the brand's value, and + * it defaults to `never`. + */ +export interface Brand { readonly [BRAND]: B; } @@ -27,29 +42,111 @@ export interface Flavor { export type Flavored = [F] extends [NEVER] ? V & Flavor : V | Flavored; -// deno-lint-ignore ban-types -export type strings = string & {}; - export type properties = Flavored; export type KeyOf = | (Strict extends true ? never : strings) | (string & keyof T); -export type PropKeys = - | (Strict extends true ? never : properties) - | (PropertyKey & keyof T); - -// deno-lint-ignore no-explicit-any -export type AbstractConstructor = - abstract new (...args: A) => T; +export type AbstractConstructor = abstract new (...args: A) => T; -// deno-lint-ignore no-explicit-any -export type Constructor = { +export type Constructor = { new (...args: A): T; }; export type FunctionKeys = keyof { - // deno-lint-ignore no-explicit-any - [K in keyof T as T[K] extends (...args: any[]) => any ? K : never]: K; + [K in keyof T as T[K] extends (...args: any) => any ? K : never]: K; }; + +/** + * Represents an accessor method's property descriptor. which may only have a + * getter and/or setter, a `configurable` flag, and an `enumerable` flag. The + * `value` and `writable` flags are not allowed. + * + * @template T The type of the accessor's value. + * @category Types + */ +export interface AccessorPropertyDescriptor { + get?(): T; + set?(value: T): void; + configurable?: boolean; + enumerable?: boolean; +} + + +export type As = T extends U ? U extends T ? U : T + : [T & U] extends [never] ? U & Omit + : T & U; + +export type Is = Or, U>; + +//T extends U ? U extends T ? U : T +// : [T & U] extends [never] ? U +// : T & U; + +const _: unique symbol = Symbol("_"); +type _ = typeof _; + +export type strings = string & { [_]?: "string" | void }; + +export type numbers = number & { [_]?: "number" | void }; + +export type symbols = symbol & { [_]?: "symbol" | void }; + +export type PropertyKeys = strings | numbers | symbols; + +export type ValueOf = T[keyof T]; + +export type PropKeys = + | (Strict extends true ? never : properties) + | (PropertyKey & keyof T); + +export type Class< + Prototype extends object | null = any, + Args extends readonly unknown[] = readonly any[], + Static extends {} = {}, +> = + & Constructor + & { readonly prototype: Prototype } + & ({} extends Static ? unknown : { + [K in keyof Static as [Static[K]] extends [never] ? never : K]: + & ThisType> + & Static[K]; + }); + +export type Or = ([A & {}] extends [never] ? B : A) extends + infer V extends B ? V : never; + +export type AbstractClass< + Prototype extends object | null = any, + Args extends readonly unknown[] = readonly any[], + Static extends {} = {}, +> = + & AbstractConstructor + & { readonly prototype: Prototype } + & ( + {} extends Static ? unknown + : { + [K in keyof Static as [Static[K]] extends [never] ? never : K]: + & ThisType> + & Static[K]; + } + ); + +// deno-fmt-ignore +export type ParametersOf = + | [T] extends [never] ? Fallback + : readonly [...ParametersOfWorker, Fallback>]; + +// deno-fmt-ignore +type ParametersOfWorker = + | T extends (...args: infer A) => any ? Readonly : Fallback; + +export type Voidable = T | void; + +export type VoidableArgument = boolean | void; + +export type MaybeVoidable = ( + | (void extends V ? void : T) + | ([V] extends [never] ? void : V extends true ? void : never) +) extends infer U ? U : never; From ffef07834ed81ec1a45c23b078bc832ad252caad Mon Sep 17 00:00:00 2001 From: Nicholas Berlette Date: Tue, 14 May 2024 13:36:24 -0700 Subject: [PATCH 03/11] chore(types): add readme, license, and package config --- packages/types/LICENSE | 21 ++ packages/types/README.md | 439 ++++++++++++++++++++++++++++++++++++ packages/types/_internal.ts | 97 ++++++++ packages/types/deno.json | 36 +++ packages/types/mod.ts | 69 ++++++ 5 files changed, 662 insertions(+) create mode 100644 packages/types/LICENSE create mode 100644 packages/types/README.md create mode 100644 packages/types/_internal.ts create mode 100644 packages/types/deno.json create mode 100644 packages/types/mod.ts diff --git a/packages/types/LICENSE b/packages/types/LICENSE new file mode 100644 index 0000000..c34c878 --- /dev/null +++ b/packages/types/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Nicholas Berlette (https://github.com/nberlette) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/types/README.md b/packages/types/README.md new file mode 100644 index 0000000..28ddf18 --- /dev/null +++ b/packages/types/README.md @@ -0,0 +1,439 @@ +# [![][@]decorators/types][@decorators/types] + +The **`@decorators/types`** package provides a comprehensive set of TypeScript +types, type guards, and other utilities for working with TS/JS Decorators. + +## Features + +- **Stage 3 (ES) Decorators**: + - `ClassDecoratorFunction`[^1] + - `ClassFieldDecoratorFunction`[^2] + - `ClassMethodDecoratorFunction`[^3] + - `ClassAccessorDecoratorFunction`[^4] + - `ClassGetterDecoratorFunction`[^5] + - `ClassSetterDecoratorFunction`[^6] +- **Legacy Decorators** (`experimentalDecorators`[^8]) + - `LegacyClassDecoratorFunction` + - `LegacyPropertyDecoratorFunction` + - `LegacyMethodDecoratorFunction` + - `LegacyAccessorDecoratorFunction` +- **Type Guards** (Predicates): + - `isDecoratorArguments` + - `isDecoratorContext` + - `isLegacyDecoratorArguments` + - `isIsomorphicDecoratorArguments` +- **Type Utilities**: + - `DecoratorName` + - `DecoratorThis` + - `DecoratorValue` + - `DecoratorReturn` + - `DecoratorArguments` + - Isomorphic[^7] variants: `AnyDecorator[This|Value|Name|Arguments|Return]` + - Legacy[^8] variants: `LegacyDecorator[This|Value|Name|Arguments|Return]` + +Includes types for both Legacy Decorators (Stage 2, `experimentalDecorators`), +and Stage 3 Decorators (ES Decorators), including fully-typed signatures for +each of the different kinds of decorators, their respective arguments, and the +return types of the actual decorator functions themselves. + +This allows you to write isomorphic[^7] decorators that work in both Legacy[^8] +(Stage 2) and modern Stage 3 environments, without needing to maintain two +separate implementations. + +> **Note**: currently, parameter decorators are only r + +[^1]: `ClassDecoratorFunction` is the signature for decorators corresponding to +the context type `ClassDecoratorContext` (included in TypeScript v5.x+). + +[^2]: `ClassFieldDecoratorFunction` is the signature for decorators +corresponding to the context type `ClassFieldDecoratorContext` (included in +TypeScript v5.x+). + +[^3]: `ClassMethodDecoratorFunction` is the signature for decorators +corresponding to the context type `ClassMethodDecoratorContext` (included in +TypeScript v5.x+). + +[^4]: `ClassAccessorDecoratorFunction` is the signature for decorators +corresponding to the context type `ClassAccessorDecoratorContext` (included in +TypeScript v5.x+). + +[^5]: `ClassGetterDecoratorFunction` is the signature for decorators +corresponding to the context type `ClassGetterDecoratorContext` (included in +TypeScript v5.x+). + +[^6]: `ClassSetterDecoratorFunction` is the signature for decorators +corresponding to the context type `ClassSetterDecoratorContext` (included in +TypeScript v5.x+). + +[^7]: "Isomorphic Decorators" (a term used throughout this project) describes a +special type of decorator, that is compatible with the current iteration of the +Decorators Proposal (Stage 3), as well as the legacy[^8] (Stage 2, +"experimentalDecorators") implementation in TypeScript. This allows you to write +decorators that work in both environments, without needing to maintain two +separate implementations. + +[^8]: "Legacy Decorators" refer to TypeScript's implementation of a previous +iteration of the TC39 Decorators Proposal, which never surpassed Stage 2 (also +commonly known as Stage 2 or "experimental decorators"). These require the +"experimentalDecorators"[^9] option to be enabled in your TypeScript compiler +options configuration. + +[^9]: Before TypeScript v4.x, these required you to enable the +"experimentalDecorators" compiler option. In TypeScript v4.x they are enabled +default. Starting in TypeScript v5.0, the newer Stage 3 decorators are the +default, and these legacy (Stage 2) decorators once again require the +"experimentalDecorators" option to be enabled. + +## Getting Started + +All `@decorators/*` packages are published **exclusively** to **[JSR.io][JSR]**, +a new registry for TypeScript and JavaScript developers, with a keen focus on +performance, security, and compatibility. + +### Install + +[Deno][deno] + +```sh +deno add @decorators/types +``` + +[JSR][jsr] + +```sh +jsr add @decorators/types +``` + +NPM + +```sh +npx jsr add @decorators/types +``` + +[Bun][bun] + +```sh +bunx jsr add @decorators/types +``` + +[PNPM][pnpm] + +```sh +pnpx jsr add @decorators/types +``` + +[Yarn][yarn] + +```sh +yarn dlx jsr add @decorators/types +``` + +--- + +### Features + +Stage 3 Decorators are located in the root of the package: + +| Feature | Path | +| :------------------------------- | :------------ | +| `ClassDecoratorFunction` | `./class` | +| `ClassFieldDecoratorFunction` | `./field` | +| `ClassMethodDecoratorFunction` | `./method` | +| `ClassAccessorDecoratorFunction` | `./accessor` | +| `ClassGetterDecoratorFunction` | `./getter` | +| `ClassSetterDecoratorFunction` | `./setter` | +| **Predicate Functions** | `./guards` | +| **Type Utilities** | `./utilities` | + +Legacy Decorators are located in the `legacy` directory: + +| Feature | Import Path | +| :-------------------------------- | :------------------- | +| `LegacyClassDecoratorFunction` | `./legacy/class` | +| `LegacyPropertyDecoratorFunction` | `./legacy/field` | +| `LegacyMethodDecoratorFunction` | `./legacy/method` | +| `LegacyAccessorDecoratorFunction` | `./legacy/accessor` | +| **Predicate Functions** | `./legacy/guards` | +| **Type Utilities** | `./legacy/utilities` | + +Isomorphic types (supporting both Legacy and Stage 3 decorators) are located in +the root of the package (alongside the Stage 3 types). They are easily +distinguished from the other types by their `Any` prefix. + +For the best results, it's recommended that you pick one of the following 3 +conventions for your decorator implementations, and utilize the types that are +within that same column. + +Meaning, if you're creating a legacy decorator package, stick to using the types +in the `Legacy` category. If you're creating a Stage 3 decorator package without +Legacy support, use the types in the `Stage 3` column. If you're experimenting +with both and wish to support either environment, or if you're unsure of your +user's environment, use the `Isomorphic` types. + +| Stage 3 | Legacy | Isomorphic | +| :------------------: | :------------------------: | :---------------------: | +| `DecoratorArguments` | `LegacyDecoratorArguments` | `AnyDecoratorArguments` | +| `DecoratorReturn` | `LegacyDecoratorReturn` | `AnyDecoratorReturn` | +| `DecoratorThis` | `LegacyDecoratorThis` | `AnyDecoratorThis` | +| `DecoratorValue` | `LegacyDecoratorValue` | `AnyDecoratorValue` | +| `DecoratorName` | `LegacyDecoratorName` | `AnyDecoratorName` | + +### Examples + +Here's a quick example of how you can use the `@decorators/types` package to +create an isomorphic memoization decorator that can be used in both legacy and +Stage 3 environments, without sacrificing type safety or code quality. + +```ts +import type { + AnyDecoratorArguments, // union of all possible decorator argument types + AnyDecoratorReturn, // determines correct return type from decorator args + AnyDecoratorThis, // determines contextual `this` type from decorator args + AnyDecoratorValue, // determines decorator value type from decorator args +} from "@decorators/types"; + +/** + * Isomorphic memoization decorator that can be used on any class member or on + * a class constructor itself. + */ +function memoize< + const Args extends AnyDecoratorArguments, +> // If you need to access the `this` type, you can use this type: +// This extends AnyDecoratorThis, +// If you need to access the value type, you can use this type: +// Value extends AnyDecoratorValue, +( + options?: MemoizeOptions, +): (...args: [...Args]) => AnyDecoratorReturn { + return (target, context) => { + switch (context.kind) { + case "class": // ... handle class decorators ... + break; + case "field": // ... handle field decorators ... + break; + // ... implement additional target types here ... + } + }; +} +``` + +--- + +## API + +### ![][badge-function] `isDecoratorArguments` + +Checks if a given tuple is a valid set of Stage 3 Decorator arguments. This is a +rigorous type guard that ensures the tuple is a valid set of arguments for a +decorator function, cross-checking the expected `target` type against the kind +of decorator as determined by the `context.kind` value. + +```ts +import { + type AnyDecoratorArguments, + type AnyDecoratorReturn, + isDecoratorArguments, +} from "@decorators/types"; + +const isomorphic = ( + ...args: Args +): AnyDecoratorReturn => { + if (isDecoratorArguments(args)) { + // stage 3 decorator arguments detected (i.e. [target, context]). + } else { + // either legacy decorators, or invalid arguments. + // perform additional checks here for legacy implementation. + } +}; + +// This decorator will now work in both legacy and stage 3 environments: +@isomorphic +class MyClass {} +``` + +--- + +### ![][badge-function] `isLegacyDecoratorArguments` + +Checks if a given tuple is a valid set of legacy TypeScript Decorator arguments. +This is a rigorous type guard that ensures the tuple is a valid set of arguments +for a legacy (stage 2, experimental) TypeScript decorator function, and not +those of a Stage 3 Decorator or a different type of function. + +```ts +import { + type AnyDecoratorArguments, + isLegacyDecoratorArguments, +} from "@decorators/types"; + +const isomorphic = (...args: LegacyDecoratorArguments) => { + if (isLegacyDecoratorArguments(args)) { + // legacy TypeScript decorator arguments detected (i.e. [target, key, desc]). + } else { + // either stage 3 decorators, or invalid arguments. + // perform additional checks here for stage 3 implementation. + } +}; + +// This decorator will now work in both legacy and stage 3 environments: +@isomorphic +class MyClass {} +``` + +--- + +### ![][badge-function] `isAnyDecoratorArguments` + +Checks if a given tuple is a valid set of Stage 3 Decorator arguments _or_ a +valid set of legacy TypeScript Decorator arguments. This is a simple logical OR +operation that relies on the `isDecoratorArguments` and +`isLegacyDecoratorArguments` functions to determine the type of decorator +arguments provided. It is highly recommended that you use one or both of those +functions in addition to this one, to narrow down the input type as accurately +as possible. + +```ts +import { + type AnyDecoratorArguments, + isAnyDecoratorArguments, +} from "@decorators/types"; + +const isomorphic = (...args: ) => { + if (isAnyDecoratorArguments(args)) { + // stage 3 or legacy TypeScript decorator arguments detected. + // proceed with further checks to determine the exact type. + } else { + throw new TypeError("Invalid decorator arguments provided."); + } +}; +``` + +--- + +### ![][badge-function] `isDecoratorContext` + +Checks if a given value is a valid Stage 3 Decorator context object. This will +return `true` only if the object has a `kind` property that matches one of the +valid decorator kinds, an `addInitializer` method, a `name` property (that is +either a `string` or `undefined` if `kind` is `"class"`, or a `string` or +`symbol` otherwise). + +If the `kind` property is _not_ `"class"`, the object must also have a `private` +property that is a boolean, a `static` property that is also a boolean, and an +`access` property that is an object with `has`, `get`, or `set` properties +(depending on the value of `kind`). + +```ts +import { isDecoratorContext } from "@decorators/types"; + +const isomorphic = (target: unknown, context: unknown) => { + if (isDecoratorContext(context)) { + // stage 3 decorator context detected. + } else { + // perform additional checks here for legacy implementation. + } +}; +``` + +--- + +### ![][badge-type] `DecoratorArguments` + +Represents the arguments for all **Stage 3 Decorator** kinds, as a union of +tuple pairs. This is a **generic type**[^10] with two (optional) parameters: + +1. `This` - the type of the contextual `this` binding for . For class decorators + and those applied to static members, this will be the type of the class + itself (e.g. `typeof ClassName`). In decorators applied to instance members, + this will be the type of the class instance (e.g. `ClassName`). +2. `Value` - the type of the value being decorated. This is the type of the + propeerty, method, or accessor being decorated, and is + +This type can be used in conjunction with the `isDecoratorArguments` function +and `DecoratorReturn` type to create an overloaded decorator or decorator +factory function, by inferring the type of decorator being used based on the +specific arguments the function is called with at runtime. We leverage the +compile-time information about those same arguments to determine the correct +return type, ensuring type safety is maintained throughout the entire process. + +```ts +import type { DecoratorArguments, DecoratorReturn } from "@decorators/types"; +``` + +```ts +import type { + AnyDecoratorArguments, + AnyDecoratorReturn, +} from "@decorators/types"; + +// isomorphic decorator +function memoize( + ...args: Args +): AnyDecoratorReturn { + // ... implementation here ... +} + +// If you need to pass outer arguments to the decorator, you can use +// isomorphic decorator factory +function memoizeFactory( + options?: MemoizeOptions, +): (...args: Args) => AnyDecoratorReturn { + // ... implementation here ... +} + +@memoize @memoizeFactory() +class Foo { + @memoize + bar = 42; + + @memoizeFactory({ maxAge: 1000 }) + baz() { + return "qux"; + } + + @memoize + @memoizeFactory({}) + get quux() { + return Math.random() * 1e6 | 0; + } +} +``` + +--- + +
+ +##### **[MIT]** © **[Nicholas Berlette]**. All rights reserved. + +###### [Read the Docs][Docs] · [View Source][GitHub] · [Squash a Bug][Issues] + +
+ +[![][badge-jsr-decorators] ![][badge-jsr] ![][badge-jsr-score]][jsr] + +
+ +[@decorators/types]: https://github.com/nberlette/decorators/tree/main/packages/types#readme "Check out '@decorators/types' and more decorator packages in our GitHub monorepo!" +[GitHub]: https://github.com/nberlette/decorators/tree/main/packages/types#readme "Check out '@decorators/types' and more decorator packages in our GitHub monorepo!" +[MIT]: https://nick.mit-license.org "MIT © 2024+ Nicholas Berlette. All rights reserved." +[Nicholas Berlette]: https://github.com/nberlette "Nicholas Berlette's GitHub Profile" +[Issues]: https://github.com/nberlette/decorators/issues/new?assignees=nberlette&labels=types,bugs&title=%5Btypes%5D+ "Found a bug? Let's squash it!" +[deno]: https://deno.land "Deno's Official Website" +[jsr]: https://jsr.io/@decorators/types "View the @decorators/types package on jsr.io/@decorator/types" +[docs]: https://jsr.io/@decorators/types "View @decorators API docs" +[`1.40.0` release]: https://deno.land/blog/1.40 "Deno 1.40.0 Release Notes" +[TC39 Decorators Proposal]: https://github.com/tc39/proposal-decorators "TC39 Proposal: Decorators" +[bun]: https://bun.sh "Bun: a new runtime, bundler, package manager for TypeScript / TSX" +[pnpm]: https://pnpm.io "PNPM Package Manager" +[yarn]: https://yarnpkg.com "Yarn Package Manager" +[@]: https://api.iconify.design/streamline:mail-sign-at-email-at-sign-read-address.svg?width=2.5rem&height=1.4rem&color=%23fa0 +[badge-jsr-decorators]: https://jsr.io/badges/@decorators "jsr.io/@decorators" +[badge-jsr]: https://jsr.io/badges/@decorators/types "jsr.io/@decorators/types" +[badge-jsr-score]: https://jsr.io/badges/@decorators/types/score "jsr.io/@decorators/types" +[badge-pnpm]: https://api.iconify.design/logos:pnpm.svg?height=1.3rem&inline=true "PNPM" +[badge-bun]: https://api.iconify.design/logos:bun.svg?height=1.5rem&inline=true "Bun" +[badge-yarn]: https://api.iconify.design/logos:yarn.svg?height=1.5rem&inline=true "Yarn" +[badge-npm-2]: https://api.iconify.design/logos:npm.svg?height=1rem&inline=true "NPM" +[badge-deno-2]: https://api.iconify.design/logos:deno.svg?height=2rem&width=2.5rem&inline=true +[badge-function]: https://api.iconify.design/tabler:square-rounded-letter-f-filled.svg?height=1.3rem&color=%232986ff +[badge-type]: https://api.iconify.design/tabler:square-rounded-letter-t-filled.svg?height=1.3rem&color=orchid diff --git a/packages/types/_internal.ts b/packages/types/_internal.ts new file mode 100644 index 0000000..fed2221 --- /dev/null +++ b/packages/types/_internal.ts @@ -0,0 +1,97 @@ +// deno-lint-ignore-file no-explicit-any + +import type { ClassAccessorDecoratorFunction } from "./accessor.ts"; +import type { ClassDecoratorFunction } from "./class.ts"; +import type { ClassFieldDecoratorFunction } from "./field.ts"; +import type { ClassGetterDecoratorFunction } from "./getter.ts"; +import type { LegacyAccessorDecoratorFunction } from "./legacy/accessor.ts"; +import type { LegacyClassDecoratorFunction } from "./legacy/class.ts"; +import type { LegacyMethodDecoratorFunction } from "./legacy/method.ts"; +import type { LegacyParameterDecoratorFunction } from "./legacy/parameter.ts"; +import type { LegacyPropertyDecoratorFunction } from "./legacy/property.ts"; +import type { ClassMethod, ClassMethodDecoratorFunction } from "./method.ts"; +import type { ClassSetterDecoratorFunction } from "./setter.ts"; +import type { + Constructor, + Is, + KeyOf, + Or, + ValueOf, +} from "jsr:@decorators/internal@^0.1.3"; +export * from "jsr:@type/union@^0.1.0"; +export * from "jsr:@decorators/internal@^0.1.3/assert"; +export * from "jsr:@decorators/internal@^0.1.3/types"; +export type { Is } from "jsr:@decorators/internal@^0.1.3/types"; + +/** + * Extracts the Parameters of a given decorator. + */ +export type DecoratorParameters = T extends + (...args: infer A) => any + ? Is, Readonly>>> + : Fallback; + +export interface DecoratorTypeMap< + This = any, + Value = any, +> { + "class": ClassDecoratorFunction< + InstanceType>, + Is + >; + "field": ClassFieldDecoratorFunction; + "accessor": ClassAccessorDecoratorFunction; + "getter": ClassGetterDecoratorFunction; + "setter": ClassSetterDecoratorFunction; + "method": ClassMethodDecoratorFunction>>; +} + +export interface LegacyDecoratorTypeMap< + This = any, + Value = any, + Key extends KeyOf> = KeyOf>, +> { + "legacy:class": LegacyClassDecoratorFunction< + InstanceType>, + Is + >; + "legacy:parameter": LegacyParameterDecoratorFunction, Key>; + "legacy:field": LegacyPropertyDecoratorFunction, Key>; + "legacy:accessor": LegacyAccessorDecoratorFunction< + Is, + Value, + Key + >; + "legacy:method": LegacyMethodDecoratorFunction, Value, Key>; +} + +export type AnyDecoratorTypeMap< + This = any, + Value = any, + Key extends KeyOf> = KeyOf>, +> = + & DecoratorTypeMap + & LegacyDecoratorTypeMap; + +export type DecoratorArgsTypeMap< + This = any, + Value = any, + Key extends KeyOf> = KeyOf>, + AsEntries extends boolean = false, + Map extends Record = AnyDecoratorTypeMap< + This, + Value, + Key + >, +> = { + readonly [P in keyof Map]-?: [AsEntries] extends [true] + ? [Map[P], Readonly>] + : Readonly>; +} extends infer V ? V : never; + +export function __throw(error: string | Error): never { + error = typeof error === "string" ? new Error(error) : error; + Error.captureStackTrace?.(error, __throw); + error.stack?.slice(); + throw error; +} diff --git a/packages/types/deno.json b/packages/types/deno.json new file mode 100644 index 0000000..2cd7ce2 --- /dev/null +++ b/packages/types/deno.json @@ -0,0 +1,36 @@ +{ + "name": "@decorators/types", + "version": "0.1.2", + "exports": { + ".": "./mod.ts", + "./class": "./class.ts", + "./method": "./method.ts", + "./field": "./field.ts", + "./accessor": "./accessor.ts", + "./getter": "./getter.ts", + "./setter": "./setter.ts", + "./guards": "./guards.ts", + "./utilities": "./utilities.ts", + "./signatures": "./signatures.ts", + "./legacy": "./legacy.ts", + "./legacy/class": "./legacy/class.ts", + "./legacy/guards": "./legacy/guards.ts", + "./legacy/method": "./legacy/method.ts", + "./legacy/property": "./legacy/property.ts", + "./legacy/accessor": "./legacy/accessor.ts", + "./legacy/parameter": "./legacy/parameter.ts", + "./legacy/utilities": "./legacy/utilities.ts" + }, + "publish": { + "include": [ + "**/*.ts", + "*.json", + "*.md", + "LICENSE" + ], + "exclude": [ + "*.test.*", + "**/test*" + ] + } +} diff --git a/packages/types/mod.ts b/packages/types/mod.ts new file mode 100644 index 0000000..d29d417 --- /dev/null +++ b/packages/types/mod.ts @@ -0,0 +1,69 @@ +/** + * @module types + * + * The `@decorators/types` package provides a collection of utility types and + * runtime type guards for working with both modern and legacy decorators in a + * TypeScript codebase. These types are designed to simplify the creation, use, + * testing, and maintenance of decorators and decorator factories by providing + * a consistent, well-documented, and type-safe foundation for their respective + * API surfaces. + * + * An immediate use case for these types is to minimalize boilerplate code + * required to define decorators that are cross-compatible with both the legacy + * and stage 3 forms of the Decorators Proposal, as well as creating decorators + * that are usable on multiple targets (e.g., classes, methods, fields, etc.). + * + * Such a task would typically involve creating a decorator factory that + * returned a decorator with a specific signature for each of the supported + * target types. This could quickly become cumbersome and error-prone without + * some outside help; but with a package like `@decorators/types`, the process + * suddenly becomes much more manageable: + * + * ```ts + * import { + * isDecoratorArguments, + * isLegacyDecoratorArguments, + * type AnyDecoratorArguments, + * type AnyDecoratorReturn, + * } from "@decorators/types"; + * + * const log = >( + * prefix?: string + * ): (...args: [...Args]) => AnyDecoratorReturn => { + * return (...args) => { + * if (isDecoratorArguments(args)) { + * // Stage 3 Decorators implementation ... + * // args -> [target, context] + * console.log(`[${prefix}] Initializing ${String(args[1].name?.toString())}`); + * } else if (isLegacyDecoratorArguments(args)) { + * // Legacy Decorators implementation ... + * // args -> [target, key, descriptor?] + * console.log(`[${prefix}] Initializing ${String(args[1].toString())}`); + * } else { + * throw new TypeError("Invalid decorator arguments"); + * } + * }; + * }; + * + * // the `log` decorator can now be used on any target type, in both + * // legacy and stage 3 environments, with full type safety and consistency: + * + * @log("class") class Example { + * // ... + * + * @log("method") method() { return "foo" } + * + * @log("getter") get field() { return 42 } + * } + * ``` + */ +export * from "./accessor.ts"; +export * from "./class.ts"; +export * from "./field.ts"; +export * from "./getter.ts"; +export * from "./guards.ts"; +export * from "./legacy.ts"; +export * from "./method.ts"; +export * from "./setter.ts"; +export * from "./signatures.ts"; +export * from "./utilities.ts"; From efa985a1d298c3a55ba27eff4879689224e7bf78 Mon Sep 17 00:00:00 2001 From: Nicholas Berlette Date: Tue, 14 May 2024 14:23:37 -0700 Subject: [PATCH 04/11] chore(internal): bump @decorators/internal -> 0.1.3 --- internal/deno.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/deno.json b/internal/deno.json index 0885678..d641516 100644 --- a/internal/deno.json +++ b/internal/deno.json @@ -1,6 +1,6 @@ { "name": "@decorators/internal", - "version": "0.1.2", + "version": "0.1.3", "exports": { ".": "./mod.ts", "./assert": "./assert.ts", From d825c666281c045e168efd0017680b2d262a243f Mon Sep 17 00:00:00 2001 From: Nicholas Berlette Date: Wed, 15 May 2024 14:10:30 -0700 Subject: [PATCH 05/11] chore: update guards.ts, signatures.ts, utilities.ts --- packages/types/guards.ts | 202 +++++++++++++++++++++++++ packages/types/signatures.ts | 241 ++++++++++++++++++++++++++++++ packages/types/utilities.ts | 277 +++++++++++++++++++++++++++++++++++ 3 files changed, 720 insertions(+) create mode 100644 packages/types/guards.ts create mode 100644 packages/types/signatures.ts create mode 100644 packages/types/utilities.ts diff --git a/packages/types/guards.ts b/packages/types/guards.ts new file mode 100644 index 0000000..e75091a --- /dev/null +++ b/packages/types/guards.ts @@ -0,0 +1,202 @@ +// deno-lint-ignore-file no-explicit-any +import type { DecoratorArgsTypeMap } from "./_internal.ts"; +import { isLegacyDecoratorArguments } from "./legacy/guards.ts"; +import type { AnyDecoratorArguments, DecoratorArguments } from "./utilities.ts"; + +/** + * Checks if the provided arguments are compatible with a Stage 3 Decorator + * function, returning `true` if they are, and `false` if they are not. + * + * To check if the arguments are compatible with a legacy decorator function, + * see {@link isLegacyDecoratorArguments} instead. + * + * @template {readonly unknown[]} A The tuple of decorator arguments. + * @param args The tuple of decorator arguments to check. + * @returns {args is DecoratorArguments} `true` if the arguments are compatible + * with a Stage 3 decorator function, `false` otherwise. + * @example + * ```ts + * import { isDecoratorArguments } from "@decorators/types"; + * + * function decorator(...args: any[]) { + * if (isDecoratorArguments(args)) { + * // called as a Stage 3 decorator with no args + * return decoratorStage3(...args); + * } else { + * // called as a decorator factory with args + * return (target, context) => decoratorStage3(target, context); + * } + * } + * ``` + * @category Utilities + */ +export function isDecoratorArguments( + args: [...A], +): args is [...Extract>]; + +/** + * Checks if the provided arguments are compatible with a Stage 3 Decorator + * function, returning `true` if they are, and `false` if they are not. + * + * To check if the arguments are compatible with a legacy decorator function, + * see {@link isLegacyDecoratorArguments} instead. + * + * @template {readonly unknown[]} A The tuple of decorator arguments. + * @param args The tuple of decorator arguments to check. + * @returns {args is DecoratorArguments} `true` if the arguments are compatible + * with a Stage 3 decorator function, `false` otherwise. + * @example + * ```ts + * import { isDecoratorArguments } from "@decorators/types"; + * + * function decorator(...args: any[]) { + * if (isDecoratorArguments(args)) { + * // called as a Stage 3 decorator with no args + * return decoratorStage3(...args); + * } else { + * // called as a decorator factory with args + * return (target, context) => decoratorStage3(target, context); + * } + * } + * ``` + * @category Utilities + */ +export function isDecoratorArguments< + This, + Value, +>(args: unknown): args is DecoratorArguments; + +/** @ignore */ +export function isDecoratorArguments( + args: unknown, +): args is DecoratorArguments { + if (Array.isArray(args) && args.length === 2) { + const [target, context] = args; + return isDecoratorContext(context) && isValidTarget(target, context); + } + return false; +} + +/** + * Checks if the provided arguments are compatible with **any** decorator type, + * whether it be a Stage 3 decorator or a Legacy Stage 2 decorator. This uses a + * logical OR of {@link isDecoratorArguments} and {@link isLegacyDecoratorArguments} to + * determine if the arguments are compatible with either type of decorator. + * + * @template {readonly unknown[]} A The tuple of decorator arguments. + * @param args The tuple of decorator arguments to check. + * @returns {args is AnyDecoratorArguments} `true` if the arguments are + * compatible with any decorator type, `false` otherwise. + * @category Utilities + */ +export function isAnyDecoratorArguments< + This, + Value, +>(args: unknown): args is AnyDecoratorArguments { + return isDecoratorArguments(args) || isLegacyDecoratorArguments(args); +} + +/** + * Checks if the provided context object is a valid decorator context object. + * + * **Note**: this is only compatible with Stage 3 decorators, and will return + * `false` if used with legacy decorator arguments. + * + * @template {DecoratorContext} T The type of decorator context to check. + * @param context The context object to check. + * @returns {context is T} `true` if the context object is valid, `false` otherwise. + */ +export function isDecoratorContext( + context: unknown, +): context is T { + if ( + typeof context === "object" && context != null && !Array.isArray(context) + ) { + if ( + "kind" in context && "name" in context && "addInitializer" in context + ) { + const kinds = [ + "class", + "field", + "method", + "getter", + "setter", + "accessor", + ]; + const { kind, name, addInitializer } = + (context ?? {}) as DecoratorContext; + if ( + (typeof addInitializer !== "function") || + (typeof kind !== "string" || !kinds.includes(kind)) || + ( + typeof name !== "string" && + typeof name !== ( + // class decorators can have a `name` of `string | undefined`. + // all others can be `string | symbol`. + kind === "class" ? "undefined" : "symbol" + ) + ) || (kind !== "class" && !( + "private" in context && typeof context.private === "boolean" && + "static" in context && typeof context.static === "boolean" && + "access" in context && typeof context.access === "object" && + context.access != null && !Array.isArray(context.access) + )) + ) return false; + const { access = {} } = context as ClassMemberDecoratorContext; + return !( + typeof access === "object" && access != null && + ("has" in access && typeof access.has === "function") && + (typeof (access as any).get === ( + ["accessor", "getter", "field", "method"].includes(kind) && + "get" in access + ? "function" + : "undefined" + )) && + (typeof (access as any).set === ( + ["accessor", "setter", "field", "method"].includes(kind) && + "set" in access + ? "function" + : "undefined" + )) + ); + } + } + return false; +} + +/** + * Checks if the provided target is a valid target for the given decorator + * context. This utility is useful for verifying that the target received by + * an overloaded stage 3 decorator function is compatible with the context + * object it receives as well. + * + * **Note**: this is only compatible with Stage 3 decorators, and will return + * `false` if used with legacy decorator arguments. + * + * @template {DecoratorContext} T The type of decorator context to check. + * @param target The target to check. + * @param context The context object to check against + * @returns {target is DecoratorArgsTypeMap[T["kind"]][0]} `true` if the target + * is valid for the given context, `false` otherwise. + * @category Utilities + */ +export function isValidTarget( + target: unknown, + context: T, +): target is DecoratorArgsTypeMap[T["kind"]][0] { + if (isDecoratorContext(context)) { + switch (context.kind) { + case "field": + return typeof target === "undefined"; + case "accessor": + return ( + typeof target === "object" && !!target && !Array.isArray(target) && + "get" in target && typeof target.get === "function" && + "set" in target && typeof target.set === "function" + ); + default: + return typeof target === "function"; + } + } + return false; +} diff --git a/packages/types/signatures.ts b/packages/types/signatures.ts new file mode 100644 index 0000000..f2f121a --- /dev/null +++ b/packages/types/signatures.ts @@ -0,0 +1,241 @@ +// deno-lint-ignore-file no-explicit-any +import { __throw } from "./_internal.ts"; +import type { + AnyDecoratorTypeMap, + Constructor, + DecoratorTypeMap, + Is, + LegacyDecoratorTypeMap, + ValueOf, + VoidableArgument, +} from "./_internal.ts"; +import type { ClassDecoratorFunction } from "./class.ts"; +import type { ClassFieldDecoratorFunction } from "./field.ts"; +import type { ClassAccessorDecoratorFunction } from "./accessor.ts"; +import type { ClassMethod, ClassMethodDecoratorFunction } from "./method.ts"; +import type { ClassGetterDecoratorFunction } from "./getter.ts"; +import type { ClassSetterDecoratorFunction } from "./setter.ts"; +import type { + AnyDecoratorArguments, + AnyDecoratorReturn, + AnyDecoratorThis, + AnyDecoratorValue, + DecoratorArguments, + DecoratorReturn, + DecoratorThis, + DecoratorValue, +} from "./utilities.ts"; +import { + LegacyDecoratorArguments, + LegacyDecoratorKind, + LegacyDecoratorReturn, + LegacyDecoratorThis, + LegacyDecoratorValue, +} from "./legacy.ts"; + +/** + * Represents any of the possible types of decorators that can be applied to a + * class or to class members. This is a union of all the other decorator + * types. + * + * @template [This=unknown] The type on which the class element will be + * defined. For a static class element, this is the type of the constructor. + * For non-static class elements, this will be the type of the instance. + * @template [Value=unknown] The type of the class member being decorated. + * @category Decorators + * @category Signatures + */ +export type DecoratorFunction = + // coerce to AbstractConstructor to satisfy the type constraint + | ClassDecoratorFunction< + InstanceType>, + Is + > + // coerce to ClassMethod to satisfy the type constraint + | ClassFieldDecoratorFunction + | ClassAccessorDecoratorFunction + | ClassGetterDecoratorFunction + | ClassSetterDecoratorFunction + | ClassMethodDecoratorFunction>>; + +/** + * Represents the "kind" of all possible Stage 3 Decorator types. This is used + * to differentiate between the different types of decorators based on the + * `kind` property of the context object. + * @category Signatures + */ +export type DecoratorKind = string & keyof DecoratorTypeMap; + +/** + * Represents the "kind" of all possible decorators, both legacy and stage 3. + * For Stage 3 decorators, the kind is derived from the `kind` property of the + * context object. For Legacy decorators, which do not have a context object, + * it is derived from the decorator's signature using the internal type map + * `LegacyDecoratorTypeMap`. + * @category Signatures + */ +export type AnyDecoratorKind = string & keyof AnyDecoratorTypeMap; + +/** + * Represents the signature of all the possible types of decorators that can + * be applied to a class or to class members. This is a union of all the other + * decorator types, including legacy decorators. + * + * @template [This=unknown] The type on which the class element will be + * defined. For a static class element, this is the type of the constructor. + * For non-static class elements, this will be the type of the instance. + * @template [Value=unknown] The type of the class member being decorated. + * @category Decorators + * @category Signatures + */ +export type AnyDecoratorFunction< + This = unknown, + Value = unknown, +> = ValueOf>; + +/** + * Represents a decorator with multiple overload signatures, configurable via + * the {@linkcode Kind} type parameter. + */ +export interface OverloadedDecoratorFunction< + Kind extends DecoratorKind = DecoratorKind, + Void extends VoidableArgument = true, +> { + < + Args extends Parameters[Kind]>, + This = AnyDecoratorThis, + Value = AnyDecoratorValue, + >(...args: Args): DecoratorReturn; +} + +/** + * Represents a decorator with multiple overload signatures, configurable via + * the {@linkcode Kind} type parameter. + */ +export interface AnyOverloadedDecoratorFunction< + Kind extends AnyDecoratorKind = AnyDecoratorKind, + Void extends VoidableArgument = true, +> { + < + Args extends Parameters[Kind]>, + This = AnyDecoratorThis, + Value = AnyDecoratorValue, + >(...args: Args): AnyDecoratorReturn; +} + +export interface DecoratorFactory< + Outer extends readonly unknown[] = readonly any[], + Void extends VoidableArgument = true, +> { + < + Inner extends DecoratorArguments, + This = DecoratorThis, + Value = DecoratorValue, + >(...outer: Outer): (...inner: Inner) => DecoratorReturn; +} + +export interface LegacyDecoratorFactory< + Outer extends readonly unknown[] = readonly any[], + Void extends VoidableArgument = true, +> { + < + Inner extends LegacyDecoratorArguments, + This = LegacyDecoratorThis, + Value = LegacyDecoratorValue, + >(...outer: Outer): (...inner: Inner) => LegacyDecoratorReturn; +} + +export interface AnyDecoratorFactory< + Outer extends readonly unknown[] = readonly any[], + Void extends VoidableArgument = true, +> { + < + Inner extends AnyDecoratorArguments, + This = AnyDecoratorThis, + Value = AnyDecoratorValue, + >(...outer: Outer): (...inner: Inner) => AnyDecoratorReturn; +} + +/** + * Represents a factory function that creates a new decorator function with + * the specified arguments. This is used to create decorator functions that + * require additional arguments to be passed to them. + * + * @template [Args=any[]] The type of the additional arguments to be passed to + * the decorator. + * @category Decorators + * @category Signatures + */ +export interface OverloadedDecoratorFactory< + Outer extends readonly unknown[] = any[], + Kind extends DecoratorKind = DecoratorKind, + Void extends VoidableArgument = true, +> { + /** + * @template [This=unknown] The type on which the class element will be + * defined. For a static class element, this is the type of the constructor. + * For non-static class elements, this will be the type of the instance. + * @template [Value=unknown] The type of the class member being decorated. + */ + < + Inner extends Parameters[Kind]>, + This = DecoratorThis, + Value = DecoratorValue, + >(...outer: Outer): (...inner: Inner) => DecoratorReturn; +} + +/** + * Represents a factory function that creates a new decorator function with + * the specified arguments. This is used to create decorator functions that + * require additional arguments to be passed to them. + * + * @template [Args=any[]] The type of the additional arguments to be passed to + * the decorator. + * @category Decorators + * @category Signatures + */ +export interface LegacyOverloadedDecoratorFactory< + Outer extends readonly unknown[] = any[], + Kind extends LegacyDecoratorKind = LegacyDecoratorKind, + Void extends VoidableArgument = true, +> { + /** + * @template [This=unknown] The type on which the class element will be + * defined. For a static class element, this is the type of the constructor. + * For non-static class elements, this will be the type of the instance. + * @template [Value=unknown] The type of the class member being decorated. + */ + < + Inner extends Parameters[Kind]>, + This = LegacyDecoratorThis, + Value = LegacyDecoratorValue, + >(...outer: Outer): (...inner: Inner) => LegacyDecoratorReturn; +} + +/** + * Represents a factory function that creates a new decorator function with + * the specified arguments. This is used to create decorator functions that + * require additional arguments to be passed to them. + * + * @template [Args=any[]] The type of the additional arguments to be passed to + * the decorator. + * @category Decorators + * @category Signatures + */ +export interface AnyOverloadedDecoratorFactory< + Outer extends readonly unknown[] = any[], + Kind extends AnyDecoratorKind = AnyDecoratorKind, + Void extends VoidableArgument = true, +> { + /** + * @template [This=unknown] The type on which the class element will be + * defined. For a static class element, this is the type of the constructor. + * For non-static class elements, this will be the type of the instance. + * @template [Value=unknown] The type of the class member being decorated. + */ + < + Inner extends Parameters[Kind]>, + This = AnyDecoratorThis, + Value = AnyDecoratorValue, + >(...outer: Outer): (...inner: Inner) => AnyDecoratorReturn; +} diff --git a/packages/types/utilities.ts b/packages/types/utilities.ts new file mode 100644 index 0000000..192520c --- /dev/null +++ b/packages/types/utilities.ts @@ -0,0 +1,277 @@ +// deno-lint-ignore-file no-explicit-any + +import { __throw } from "./_internal.ts"; +import type { + AbstractConstructor, + DecoratorTypeMap, + MaybeVoidable, + Or, + ValueOf, + VoidableArgument, +} from "./_internal.ts"; +import type { ClassFieldDecoratorResult } from "./field.ts"; +import type { ClassMethod } from "./method.ts"; +import type { ClassGetter } from "./getter.ts"; +import type { ClassSetter } from "./setter.ts"; +import type { + LegacyDecoratorArguments, + LegacyDecoratorName, + LegacyDecoratorReturn, + LegacyDecoratorThis, + LegacyDecoratorValue, +} from "./legacy/utilities.ts"; + +/** + * Represents all possible arguments that can be used in decorator of any + * type. + * @category Signatures + */ +export type DecoratorArguments< + This = object, + Value = unknown, +> = Readonly>>>; + +/** + * Represents all possible arguments that can be passed to a decorator + * function or a legacy decorator function. + * @category Signatures + */ +export type AnyDecoratorArguments< + This = object, + Value = unknown, +> = + | DecoratorArguments + | LegacyDecoratorArguments; + +/** + * Resolves the contextual `this` type for a decorator function, based on + * arguments provided in the {@linkcode A} tuple. If the contextual `this` + * type cannot be resolved, the {@linkcode Fallback} type (default: `unknown`) + * is returned instead. This utility is useful for inferring the type of the + * `this` context for a decorator function. + * + * **Note**: this is specifically for Stage 3 decorators, and is not capable + * of handling the arguments of legacy decorator functions. To extract the + * `this` type from **_legacy_** arguments, see + * {@link LegacyDecoratorThis}. + * + * @template {readonly unknown[]} A The tuple of decorator arguments. + * @template [Fallback=unknown] Type to use if `this` cannot be resolved. + * @category Signatures + */ +// deno-fmt-ignore +export type DecoratorThis
= + | A extends readonly [infer _, infer T extends object] + ? T extends ClassDecoratorContext any> ? C + : T extends ClassAccessorDecoratorContext ? This + : T extends ClassFieldDecoratorContext ? This + : T extends ClassGetterDecoratorContext ? This + : T extends ClassMethodDecoratorContext ? This + : T extends ClassSetterDecoratorContext ? This + : Fallback + : Fallback; + +/** + * Resolves the contextual `this` type for a decorator function, based on + * arguments provided in the {@linkcode A} tuple. If the contextual `this` + * type cannot be resolved, the {@linkcode Fallback} type (default: `unknown`) + * is returned instead. This utility is useful for inferring the type of the + * `this` context for a decorator (or legacy decorator) function that accepts + * multiple types of overloaded arguments. + * + * @template {readonly unknown[]} A The tuple of decorator arguments. + * @template [Fallback=unknown] Type to use if `this` cannot be resolved. + * @category Signatures + */ +export type AnyDecoratorThis< + A extends readonly unknown[], + Fallback = unknown, +> = Or, Or, Fallback>>; + +/** + * Resolves the decorated member's value type for a stage 3 decorator + * function, based on arguments provided in the {@linkcode A} tuple. If the + * value type cannot be resolved, the {@linkcode Fallback} type (default: + * `unknown`) is returned instead. + * + * **Note**: this is specifically for Stage 3 decorators, and is not capable + * of handling the arguments of legacy decorator functions. To extract the + * value type from **_legacy_** arguments, see + * {@link LegacyDecoratorValue}. + * + * @template {readonly unknown[]} A The tuple of decorator arguments. + * @template [Fallback=unknown] Type to use if the value type cannot be + * resolved. + * @category Signatures + */ +// deno-fmt-ignore +export type DecoratorValue = + | A extends readonly [unknown, infer T extends object] // [infer B, infer T] + ? T extends ClassDecoratorContext any> ? C + : T extends ClassAccessorDecoratorContext ? V + : T extends ClassFieldDecoratorContext ? V + : T extends ClassGetterDecoratorContext ? V + : T extends ClassSetterDecoratorContext ? V + : T extends ClassMethodDecoratorContext ? V + : Fallback + : Fallback; + +/** + * Resolves the decorated member's value type for a decorator function, based + * on arguments provided in the {@linkcode A} tuple. If the value type cannot + * be resolved, the {@linkcode Fallback} type (default: `unknown`) is returned + * instead. This utility is useful for inferring the type of the value being + * decorated by a decorator. + * + * @template {readonly unknown[]} A The tuple of decorator arguments. + * @template [Fallback=unknown] Type to use if the value type cannot be + * resolved. + * @category Signatures + */ +// deno-fmt-ignore +export type AnyDecoratorValue = + ( + | DecoratorValue + | LegacyDecoratorValue + ) extends infer V ? [V] extends [never] ? Fallback : V : Fallback; + +/** + * Resolves the key of the decorated member for a decorator function, based on + * arguments provided in the {@linkcode A} tuple. If the key cannot be + * resolved, the {@linkcode Fallback} type (default: `string | symbol | + * undefined`) is returned instead. This utility is useful for inferring the + * property key of the class member, or the name of the class itself in the + * case of a ClassDecoratorFunction, that is being decorated with a decorator. + * + * @template {readonly unknown[]} A The tuple of decorator arguments. + * @template [Fallback=string | symbol | undefined] Type to use if the key + * cannot be resolved. + * @category Signatures + */ +// deno-fmt-ignore +export type DecoratorName = + | A extends readonly [any, { name: infer N extends string | symbol }] ? N + : A extends readonly [any, { name: infer N extends string | undefined }] ? N + : A extends readonly [any, { name: string | symbol | undefined }] ? A[1]["name"] + : Fallback; + +/** + * Resolves the key of the decorated member for a decorator function, based on + * arguments provided in the {@linkcode A} tuple. If the key cannot be resolved, + * the {@linkcode Fallback} type (default: `string | symbol | undefined`) is + * returned instead. This utility is useful for inferring the property key of + * the class member, or the name of the class itself in the case of a ClassDecoratorFunction, + * that is being decorated with a decorator. + * @template {readonly unknown[]} A The tuple of decorator arguments. + * @template [Fallback=string | symbol | undefined] Type to use if the key cannot be resolved. + * @category Signatures + */ +export type AnyDecoratorName< + A extends readonly unknown[], + Fallback = string | symbol | undefined, +> = ( + | DecoratorName + | LegacyDecoratorName +) extends infer V ? [V] extends [never] ? Fallback : V : Fallback; + +/** + * Represents the return type of a decorator function. + * + * @template A The tuple of decorator arguments. + * @template Void Controls the decorator return type, allowing for the + * inclusion or exclusion of the `void` type. If set to `true`, return types + * will include the `void` union member. If false, the `void` member will be + * excluded. If set to `void`, the return type will be `void`. Default: `true`. + * @category Signatures + * @example + * ```ts + * import type { + * DecoratorReturn, + * ClassMethodDecoratorFunction, + * } from "@decorators/types"; + * + * type MyDecoratorReturn1 = DecoratorReturn< + * Parameters> + * >; // => void | ((this: unknown, ...args: any[]) => unknown) + * + * type MyDecoratorReturn2 = DecoratorReturn< + * Parameters>, + * false, // exclude void from return types + * >; // => (this: unknown, ...args: any[]) => unknown + * + * type MyDecoratorReturn3 = DecoratorReturn< + * Parameters>, + * void, // only include void in return types + * >; // => void + * ``` + * @example + * ```ts + * import type { + * DecoratorReturn, + * LegacyClassDecoratorFunction, + * } from "@decorators/types"; + * + * class FoobarClass {} + * + * type MyLegacyDecoratorReturn = DecoratorReturn< + * Parameters> + * >; // => void | typeof FoobarClass + * + * type MyLegacyDecoratorReturn2 = DecoratorReturn< + * Parameters>, + * true, // enable legacy fallback + * false, // exclude void from return types + * >; // => typeof FoobarClass + * ``` + */ +// deno-fmt-ignore +export type DecoratorReturn< + A extends readonly unknown[], + Void extends VoidableArgument = true, + Fallback = unknown, +> = + | [A] extends [[any, ClassMethodDecoratorContext]] + ? [Value] extends [(this: This, ...args: infer Args) => infer Return] + ? MaybeVoidable, Void> + : Fallback + : [A] extends [[any, ClassGetterDecoratorContext]] + ? MaybeVoidable, Void> + : [A] extends [[any, ClassSetterDecoratorContext]] + ? MaybeVoidable, Void> + : [A] extends [[any, ClassAccessorDecoratorContext]] + ? MaybeVoidable, Void> + : [A] extends [[undefined, ClassFieldDecoratorContext]] + ? MaybeVoidable, Void> + : [A] extends [ + [AbstractConstructor, ClassDecoratorContext any>] + ] ? MaybeVoidable + : Fallback; + +/** + * Determines the return type of a given decorator function based on arguments + * it receives. This utility is useful for inferring the return type of a + * decorator function that accepts multiple decorator types. + * + * @template {readonly unknown[]} A The tuple of decorator arguments. + * @template [Void=true] If set to true, return types will include the + * `void` union member. If false, the `void` member will be excluded. If + * `void`, **only** `void` is included in the return type. Default is `true`. + * @template [Fallback=unknown] Type to use if the return type cannot be found. + * @category Signatures + */ +// deno-fmt-ignore +export type AnyDecoratorReturn< + A extends readonly unknown[], + Void extends VoidableArgument = true, + Fallback = unknown, +> = ( + | LegacyDecoratorReturn + | DecoratorReturn +) extends infer V ? [V] extends [never] ? Fallback : V : Fallback; + +export type { + LegacyDecoratorName, + LegacyDecoratorReturn, + LegacyDecoratorThis, + LegacyDecoratorValue, +}; From f565f38a1d3c9096a054d1516c80546682faeb6d Mon Sep 17 00:00:00 2001 From: Nicholas Berlette Date: Thu, 16 May 2024 10:12:00 -0700 Subject: [PATCH 06/11] feat(types): add legacy decorator types --- packages/types/legacy.ts | 31 +++++ packages/types/legacy/accessor.ts | 37 ++++++ packages/types/legacy/class.ts | 21 ++++ packages/types/legacy/guards.ts | 88 +++++++++++++++ packages/types/legacy/method.ts | 27 +++++ packages/types/legacy/parameter.ts | 21 ++++ packages/types/legacy/property.ts | 21 ++++ packages/types/legacy/utilities.ts | 175 +++++++++++++++++++++++++++++ 8 files changed, 421 insertions(+) create mode 100644 packages/types/legacy.ts create mode 100644 packages/types/legacy/accessor.ts create mode 100644 packages/types/legacy/class.ts create mode 100644 packages/types/legacy/guards.ts create mode 100644 packages/types/legacy/method.ts create mode 100644 packages/types/legacy/parameter.ts create mode 100644 packages/types/legacy/property.ts create mode 100644 packages/types/legacy/utilities.ts diff --git a/packages/types/legacy.ts b/packages/types/legacy.ts new file mode 100644 index 0000000..f1e922c --- /dev/null +++ b/packages/types/legacy.ts @@ -0,0 +1,31 @@ +/** + * @module legacy + * + * This module provides generic type definitions for the legacy Stage 2 form + * of TypeScript Decorators commonly used prior to TypeScript v5.0. It is not + * recommended to use these types in new code, as they have reached the end of + * their life cycle and are no longer being actively developed or maintained. + * + * Instead, use the new Stage 3 Decorators, which have their signatures + * defined in the `./signatures.ts` module. These new decorators are more + * powerful and flexible, much easier to use, and are under active development + * by both the TypeScript team and the community at large. + * + * Unfortunately, the Stage 3 form of the Decorators Proposal does not include + * support for a ParameterDecorator equivalent. Due to the popularity and the + * proliferation of ParameterDecorator usage in the wild, it's likely that + * many projects and libraries will continue to use the legacy Stage 2 form of + * the decorators for some time to come. As such, these types are provided for + * the benefit of those who are maintaining or updating existing code. + * + * @see https://github.com/tc39/proposal-decorators + * @see https://decorators.deno.dev for a living document on the Decorators + * API + */ +export * from "./legacy/accessor.ts"; +export * from "./legacy/class.ts"; +export * from "./legacy/guards.ts"; +export * from "./legacy/method.ts"; +export * from "./legacy/parameter.ts"; +export * from "./legacy/property.ts"; +export * from "./legacy/utilities.ts"; diff --git a/packages/types/legacy/accessor.ts b/packages/types/legacy/accessor.ts new file mode 100644 index 0000000..03811e5 --- /dev/null +++ b/packages/types/legacy/accessor.ts @@ -0,0 +1,37 @@ +import type { + AbstractConstructor, + AccessorPropertyDescriptor, + KeyOf, +} from "../_internal.ts"; + +/** + * Represents a ClassFieldDecorator function in the legacy Stage 2 syntax. + * + * This type of decorator requires the compiler option + * `experimentalDecorators` be explicitly set to `true` in your + * `tsconfig.json` or `deno.json` file in TypeScript v5.0 and later. It is not + * recommended for use in new code. + * + * @template This The type of the class instance or constructor function. + * @template Value The type of the class field value. + * @template Key The type of the class field key. + * @category Legacy Decorators + * @module legacy:accessor + */ +export interface LegacyAccessorDecoratorFunction< + This extends object = object | AbstractConstructor, + // deno-lint-ignore no-explicit-any + Value = any, + Key extends KeyOf = KeyOf, +> { + ( + target: T, + key: K, + descriptor: AccessorPropertyDescriptor, + ): AccessorPropertyDescriptor; + ( + target: This, + key: Key, + descriptor: AccessorPropertyDescriptor, + ): AccessorPropertyDescriptor; +} diff --git a/packages/types/legacy/class.ts b/packages/types/legacy/class.ts new file mode 100644 index 0000000..f80eedc --- /dev/null +++ b/packages/types/legacy/class.ts @@ -0,0 +1,21 @@ +import type { Constructor } from "../_internal.ts"; + +/** + * Represents a ClassDecorator function in the legacy Stage 2 syntax. + * + * This type of decorator requires the compiler option + * `experimentalDecorators` be explicitly set to `true` in your + * `tsconfig.json` or `deno.json` file in TypeScript v5.0 and later. It is not + * recommended for use in new code. + * + * @template Class The type of the class instance or constructor function. + * @category Legacy Decorators + * @module legacy:class + */ +export interface LegacyClassDecoratorFunction< + Proto extends object = object, + Class extends Constructor = Constructor, + Return extends void | Class = void | Class, +> { + (target: Class): Return; +} diff --git a/packages/types/legacy/guards.ts b/packages/types/legacy/guards.ts new file mode 100644 index 0000000..94345e7 --- /dev/null +++ b/packages/types/legacy/guards.ts @@ -0,0 +1,88 @@ +import type { KeyOf } from "../_internal.ts"; +import type { LegacyDecoratorArguments } from "./utilities.ts"; + +/** + * Checks if the provided arguments are compatible with a legacy decorator + * function, returning `true` if they are, and `false` if they are not. + * + * To check if the arguments are compatible with a Stage 3 Decorator function, + * see {@link isDecoratorArguments} instead. + * + * @template {readonly unknown[]} A The tuple of decorator arguments. + * @param args The tuple of decorator arguments to check. + * @returns {args is LegacyDecoratorArguments} `true` if the arguments are + * compatible with a legacy decorator function, `false` otherwise. + * @category Utilities + * @example + * ```ts + * import { isLegacyDecoratorArguments } from "@decorators/types"; + * + * function decorator(...args: any[]) { + * if (isLegacyDecoratorArguments(args)) { + * // called as a legacy decorator with no args + * return decoratorLegacy(...args); + * } else { + * // called as a decorator factory with args + * return (target, key, descriptor) => decoratorLegacy(target, key, descriptor); + * } + * } + * ``` + */ +export function isLegacyDecoratorArguments< + This, + Value, + Key extends KeyOf = KeyOf, +>(args: unknown): args is LegacyDecoratorArguments; + +/** + * Checks if the provided arguments are compatible with a legacy decorator + * function, returning `true` if they are, and `false` if they are not. + * + * @template {readonly unknown[]} A The tuple of decorator arguments. + * @param args The tuple of decorator arguments to check. + * @returns {args is LegacyDecoratorArguments} `true` if the arguments are + * compatible with a legacy decorator function, `false` otherwise. + * @category Utilities + */ +export function isLegacyDecoratorArguments( + args: [...A], +): args is [...Extract>]; + +/** @ignore */ +export function isLegacyDecoratorArguments( + args: unknown, +): args is LegacyDecoratorArguments { + if (Array.isArray(args) && args.length > 0 && args.length < 4) { + const [target, key, desc] = args; + // [target] + if (args.length === 1) return typeof target === "function"; + if (args.length === 2 && typeof key === "object" && key != null) { + // fails fast if one of two args appears to be a context object + // (since that is stage 3 decorator syntax only) + return false; + } + return ( + // [target, key?, desc?] + // argument 1 (target) must always be present, duh + target != null && + // argument 2 (key) can be one of two types: + // - string or symbol (property key, descriptor is optional) + // - undefined (when targeting the class constructor function) + // -> the descriptor object must be present in this case! + (typeof key === "string" || typeof key === "symbol" || + (key == null && + (typeof target === "function" || + typeof desc === "object" && desc != null))) && + // argument 3 (descriptor) can be one of three types: + // - undefined (void, class decorators and property decorators) + // - a number (parameter index for ParameterDecorators) + // - PropertyDescriptor object (method / accessor decorators) + (desc == null || typeof desc === "number" || ( + typeof desc === "object" && !Array.isArray(desc) + )) + ); + } + + // drop everything else like it's third period french + return false; +} diff --git a/packages/types/legacy/method.ts b/packages/types/legacy/method.ts new file mode 100644 index 0000000..3e0bbbf --- /dev/null +++ b/packages/types/legacy/method.ts @@ -0,0 +1,27 @@ +import type { AbstractConstructor, KeyOf } from "../_internal.ts"; + +/** + * Represents a MethodDecorator function in the legacy Stage 2 syntax. + * + * This type of decorator requires the compiler option + * `experimentalDecorators` be explicitly set to `true` in your + * `tsconfig.json` or `deno.json` file in TypeScript v5.0 and later. It is not + * recommended for use in new code. + * + * @template This The type of the class instance or constructor function. + * @template Value The type of the class method's return value. + * @template Key The type of the class method key. + * @category Legacy Decorators + * @module legacy:method + */ +export interface LegacyMethodDecoratorFunction< + This extends object = object | AbstractConstructor, + Value = unknown, + Key extends KeyOf = KeyOf, +> { + ( + target: This, + key: Key, + descriptor: TypedPropertyDescriptor, + ): TypedPropertyDescriptor; +} diff --git a/packages/types/legacy/parameter.ts b/packages/types/legacy/parameter.ts new file mode 100644 index 0000000..9d1e75b --- /dev/null +++ b/packages/types/legacy/parameter.ts @@ -0,0 +1,21 @@ +import type { AbstractConstructor, FunctionKeys, KeyOf } from "../_internal.ts"; + +/** + * Represents a ParameterDecorator function in the legacy Stage 2 syntax. + * + * This type of decorator requires the compiler option + * `experimentalDecorators` be explicitly set to `true` in your + * `tsconfig.json` or `deno.json` file in TypeScript v5.0 and later. It is not + * recommended for use in new code. + * + * @template Target The type of the class instance or constructor function. + * @template Key The type of the class method key. + * @category Legacy Decorators + * @module parameter + */ +export interface LegacyParameterDecoratorFunction< + Target extends object = object | AbstractConstructor, + Key extends KeyOf = FunctionKeys, +> { + (target: Target, key: Key, parameterIndex: number): void; +} diff --git a/packages/types/legacy/property.ts b/packages/types/legacy/property.ts new file mode 100644 index 0000000..90f86e1 --- /dev/null +++ b/packages/types/legacy/property.ts @@ -0,0 +1,21 @@ +import type { AbstractConstructor, KeyOf } from "../_internal.ts"; + +/** + * Represents a PropertyDecorator function in the legacy Stage 2 syntax. + * + * This type of decorator requires the compiler option + * `experimentalDecorators` be explicitly set to `true` in your + * `tsconfig.json` or `deno.json` file in TypeScript v5.0 and later. It is not + * recommended for use in new code. + * + * @template This The type of the class instance or constructor function. + * @template Key The type of the class property key. + * @category Legacy Decorators + * @module legacy:property + */ +export interface LegacyPropertyDecoratorFunction< + This extends object = object | AbstractConstructor, + Key extends KeyOf = KeyOf, +> { + (target: This, key: Key): void; +} diff --git a/packages/types/legacy/utilities.ts b/packages/types/legacy/utilities.ts new file mode 100644 index 0000000..e6131f8 --- /dev/null +++ b/packages/types/legacy/utilities.ts @@ -0,0 +1,175 @@ +// deno-lint-ignore-file ban-types no-explicit-any + +import type { +AbstractConstructor, + Is, + KeyOf, + LegacyDecoratorTypeMap, + MaybeVoidable, + ValueOf, +} from "../_internal.ts"; + +export type LegacyDecoratorKind = string & keyof LegacyDecoratorTypeMap; + +/** + * Represents a decorator with multiple overload signatures, configurable via + * the {@linkcode Kind} type parameter. + */ +export interface LegacyOverloadedDecoratorFunction< + Kind extends LegacyDecoratorKind = LegacyDecoratorKind, + Voidable extends boolean | void = true, +> { + < + const Args extends Readonly< + Parameters[Kind]> + >, + This = LegacyDecoratorThis, + Value = LegacyDecoratorValue, + >(...args: Args): LegacyDecoratorReturn; +} + +/** + * Represents all possible arguments that can be used in a legacy decorator + * function of any type. + * @category Arguments + */ +export type LegacyDecoratorArguments< + This = any, + Value = any, + Key extends KeyOf> = KeyOf>, +> = Readonly>>>; + +/** + * Represents the signature of all the possible types of legacy (stage 2) + * experimental decorators. By using the type parameters on this utility type, + * you can specify the types of the `this` context, the target member's value + * being decorated as well as that member's key (if applicable). + * + * @template [This=unknown] The type of the `this` context for the decorator. + * @template [Value=unknown] The type of the target member being decorated. + * @template [Key=PropertyKey] The type of the key for the target member. + * @category Legacy Decorators + * @category Signatures + */ +export type LegacyDecoratorFunction< + This = unknown, + Value = unknown, + Key extends KeyOf> = KeyOf>, +> = ValueOf>; + +/** + * Resolves the contextual `this` type for a legacy decorator function, based + * on arguments provided in the {@linkcode A} tuple. If the contextual `this` + * type cannot be resolved, the {@link Fallback} type (default: `unknown`) is + * returned instead. + * + * **Note**: this is specifically for legacy (stage 2) decorators, and is not + * capable of handling the arguments of Stage 3 decorators. To extract the + * `this` type from **_Stage 3_** arguments, see + * {@link DecoratorThis}. + * + * @template {readonly unknown[]} A The tuple of decorator arguments. + * @template [Fallback=unknown] Type to use if `this` cannot be resolved. + * @category Signatures (Legacy) + */ +// deno-fmt-ignore +export type LegacyDecoratorThis = + | A extends readonly [infer V extends Function] ? V + : A extends readonly [infer T extends object, string | symbol | undefined] ? T + : A extends readonly [infer T extends object, string | symbol, PropertyDescriptor | void] ? T + : Fallback; + +/** + * Resolves the decorated member's value type for a legacy decorator function, + * based on arguments provided in the {@linkcode A} tuple. If the value type + * cannot be resolved, the {@linkcode Fallback} type (default: `unknown`) is + * returned instead. This utility is useful for inferring the type of the + * value being decorated by a legacy decorator. + * + * **Note**: this is specifically for legacy (stage 2) decorators, and is not + * capable of handling the arguments of Stage 3 decorators. To extract the + * value type from **_Stage 3_** arguments, see + * {@link DecoratorValue}. + * + * @template {readonly unknown[]} A The tuple of decorator arguments. + * @template [Fallback=unknown] Type to use if the value type cannot be + * resolved. + * @category Signatures (Legacy) + */ +// deno-fmt-ignore +export type LegacyDecoratorValue = + | [A] extends [never] ? Fallback + : [A] extends readonly [[infer V extends AbstractConstructor]] ? V + : [A] extends readonly [[any, string | symbol | undefined, TypedPropertyDescriptor]] ? V + : [A] extends readonly [[infer T, infer K extends string | symbol | undefined, ...([number | PropertyDescriptor | void | undefined] | [])]] + ? [K] extends [keyof T] ? T[K] + : [A[2]] extends [TypedPropertyDescriptor] ? V + : Fallback + : [A] extends readonly [[infer T, infer K extends string | symbol]] + ? [K] extends [keyof T] ? T[K] : Fallback + : Fallback; + +/** + * Determines the return type of a legacy decorator function. + * + * @template A The tuple of decorator arguments. + * @template Voidable If set to true, return types will include the `void` + * union member. If false, the `void` member will be excluded. Default is + * `true`. + * @category Signatures + * @example + * ```ts + * import type { + * LegacyDecoratorReturn, + * LegacyClassDecoratorFunction, + * } from "@decorators/types"; + * + * class FoobarClass {} + * + * type MyLegacyDecoratorReturn = LegacyDecoratorReturn< + * Parameters> + * >; // => void | typeof FoobarClass + * ``` + */ +// deno-fmt-ignore +export type LegacyDecoratorReturn< + A extends readonly unknown[], + Voidable extends boolean | void = true, + Fallback = unknown, +> = A extends readonly [infer Class extends abstract new (...args: any) => any] + ? MaybeVoidable + : A extends readonly [ + any, + string | symbol | undefined, + TypedPropertyDescriptor | void + ] ? MaybeVoidable, Voidable> + : A extends ( + | readonly [any, string | symbol | undefined, number] + | readonly [any, string | symbol] + ) ? void + : Fallback; + +/** + * Resolves the key of the decorated member for a legacy decorator function, + * based on arguments provided in the {@linkcode A} tuple. If the key cannot + * be resolved, the {@linkcode Fallback} type (default: `string | symbol | + * undefined`) is returned instead. This utility is useful for inferring the + * property key of the class member, or the name of the class itself in the + * case of a ClassDecoratorFunction, that is being decorated with a legacy decorator. + * + * **Note**: this is specifically for legacy (stage 2) decorators, and is not + * capable of handling the arguments of Stage 3 decorators. To extract the key + * from **_Stage 3_** arguments, see {@link DecoratorName}. + * + * @template {readonly unknown[]} A The tuple of decorator arguments. + * @template [Fallback=string | symbol | undefined] Type to use if the key + * cannot be resolved. + * @category Signatures (Legacy) + */ +// deno-fmt-ignore +export type LegacyDecoratorName = + | A extends readonly [any, string | symbol] ? A[1] + : A extends readonly [any, string | symbol | undefined, PropertyDescriptor] ? A[1] + : A extends readonly [any, string | symbol, number] ? A[1] + : A[0] extends { name: infer N extends string | undefined } ? N + : Fallback; From a8b091b5bfaa406e0edd3c514f1f0b6f145b6b24 Mon Sep 17 00:00:00 2001 From: Nicholas Berlette Date: Sat, 18 May 2024 11:47:16 -0700 Subject: [PATCH 07/11] feat(types): add @decorators/types to root readme --- README.md | 130 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 123 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 818106d..d94815c 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,31 @@ Monorepo for packages published under the [`@decorators/*`][JSR] scope on [JSR]. ## Packages -#### [`@decorators/alias`][@decorators/alias] +### [`@decorators/alias`][@decorators/alias] -> Alias class members to simplify stack traces. +Alias class members to simplify stack traces. + +#### Install + +Deno + +```sh +deno add @decorators/alias +``` + +[JSR][jsr] + +```sh +jsr add @decorators/alias +``` + +NPM + +```sh +npx jsr add @decorators/alias +``` + +#### Usage ```ts import { alias } from "@decorators/alias"; @@ -39,20 +61,46 @@ console.assert(foo.qux === foo.bar); // OK console.assert(foo.nurp === foo.bar); // OK ``` -#### [`@decorators/bind`][@decorators/bind] +--- + +### [`@decorators/bind`][@decorators/bind] + +Bind methods, getters, and setters to the appropriate context object, with +support for static members and inheritance. + +#### Install + +Deno + +```sh +deno add @decorators/bind +``` + +[JSR][jsr] + +```sh +jsr add @decorators/bind +``` -> Bind methods, getters, and setters to the appropriate context object, with -> support for static members and inheritance. +NPM + +```sh +npx jsr add @decorators/bind +``` + +#### Usage ```ts import { bind } from "@decorators/bind"; class Foo { - @bind bar(): Foo { + @bind + bar(): Foo { return this; } - @bind static self(): typeof Foo { + @bind + static self(): typeof Foo { return this; } } @@ -62,6 +110,73 @@ console.log(self === Foo); // true console.log(bar() instanceof Foo); // true ``` +### [`@decorators/types`][@decorators/types] + +TypeScript type guards, function signatures, and type utilities for Stage 3 and +Legacy (Stage 2 / `experimentalDecorators`) Decorators. + +#### Install + +Deno + +```sh +deno add @decorators/types +``` + +[JSR][jsr] + +```sh +jsr add @decorators/types +``` + +NPM + +```sh +npx jsr add @decorators/types +``` + +#### Usage + +```ts +import { + type AnyDecoratorArguments, + type AnyDecoratorReturn, + isDecoratorArguments, + isLegacyDecoratorArguments, +} from "@decorators/types"; + +function tag( + value: string, +): (...args: Args) => AnyDecoratorReturn; +// deno-lint-ignore no-explicit-any +function tag(value: string): (...args: any[]) => any { + return (...args) => { + if (isDecoratorArguments(args)) { + const [target, context] = args; + if (context.kind !== "class") { + throw new TypeError("@tag can only be used on classes"); + } + context.addInitializer(function () { + Object.defineProperty(this.prototype, Symbol.toStringTag, { + value, + configurable: true, + }); + }); + } else if (isLegacyDecoratorArguments(args)) { + return { + [target.name]: class extends target { + get [Symbol.toStringTag](): string { + return value; + } + }, + }[target.name]; + } else { + throw new TypeError("Invalid decorator arguments"); + } + }; +} +``` + ---
@@ -74,6 +189,7 @@ console.log(bar() instanceof Foo); // true [@decorators/alias]: https://github.com/nberlette/decorators/tree/main/packages/alias#readme "Check out '@decorators/alias' and more over at the GitHub monorepo!" [@decorators/bind]: https://github.com/nberlette/decorators/tree/main/packages/bind#readme "Check out '@decorators/bind' and more over at the GitHub monorepo!" +[@decorators/types]: https://github.com/nberlette/decorators/tree/main/packages/tag#readme "Check out '@decorators/types' and more over at the GitHub monorepo!" [GitHub]: https://github.com/nberlette/decorators/tree/main/packages/bind#readme "Check out all the '@decorators/*' packages over at the GitHub monorepo!" [MIT]: https://nick.mit-license.org "MIT © 2024+ Nicholas Berlette. All rights reserved." [Nicholas Berlette]: https://github.com/nberlette "Nicholas Berlette on GitHub" From cac01dd8d645928a76b199a173ba3e9aeee2fad1 Mon Sep 17 00:00:00 2001 From: Nicholas Berlette Date: Sat, 18 May 2024 12:49:16 -0700 Subject: [PATCH 08/11] chore: bump version to 0.1.3 --- deno.json | 5 +++-- packages/alias/deno.json | 2 +- packages/bind/deno.json | 2 +- packages/types/deno.json | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/deno.json b/deno.json index 77d3f91..265d62b 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@decorators/main", - "version": "0.1.2", + "version": "0.1.3", "tasks": { "test": "deno test --parallel --allow-all --no-check=remote --coverage=.coverage ./packages/**/* ./internal/**/*", "test:watch": "deno test --watch --parallel --allow-all --no-check=remote --coverage=.coverage ./packages/**/* ./internal/**/*", @@ -27,6 +27,7 @@ "workspaces": [ "./internal", "./packages/alias", - "./packages/bind" + "./packages/bind", + "./packages/types" ] } diff --git a/packages/alias/deno.json b/packages/alias/deno.json index eaec39a..60e6fb9 100644 --- a/packages/alias/deno.json +++ b/packages/alias/deno.json @@ -1,6 +1,6 @@ { "name": "@decorators/alias", - "version": "0.1.2", + "version": "0.1.3", "exports": "./mod.ts", "publish": { "include": [ diff --git a/packages/bind/deno.json b/packages/bind/deno.json index 1ec9a3c..b14bc5d 100644 --- a/packages/bind/deno.json +++ b/packages/bind/deno.json @@ -1,6 +1,6 @@ { "name": "@decorators/bind", - "version": "0.1.2", + "version": "0.1.3", "exports": "./mod.ts", "publish": { "include": ["*.ts", "*.md", "*.json", "LICENSE"], diff --git a/packages/types/deno.json b/packages/types/deno.json index 2cd7ce2..744a5e5 100644 --- a/packages/types/deno.json +++ b/packages/types/deno.json @@ -1,6 +1,6 @@ { "name": "@decorators/types", - "version": "0.1.2", + "version": "0.1.3", "exports": { ".": "./mod.ts", "./class": "./class.ts", From 74b781c34f098d8d30fda1bb3f95e7218f25ae2b Mon Sep 17 00:00:00 2001 From: Nicholas Berlette Date: Sun, 19 May 2024 12:01:39 -0700 Subject: [PATCH 09/11] chore: update root readme.md --- README.md | 80 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 49 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index d94815c..9132196 100644 --- a/README.md +++ b/README.md @@ -6,25 +6,35 @@ Monorepo for packages published under the [`@decorators/*`][JSR] scope on [JSR]. ## Packages -### [`@decorators/alias`][@decorators/alias] +### [`@decorators/alias`] -Alias class members to simplify stack traces. +Creates aliases for existing class members, with support for methods, getters, +setters, auto-accessors, and fields. Static members are also supported, along +with `#private` members (in environments with support for ES2022 class fields). + +> **Note**: when working with private members, the `@alias` decorator **must** +> be applied inside of the **same** enclosing class that the member is declared +> in. This is due to the way that private members are scoped in JavaScript; the + +Simplifies stack traces for improved debugging, improves code readability for a +more maintainable codebase, and reduces the boilerplate typically associated +with aliasing class members in TypeScript/JavaScript. #### Install -Deno +Deno ```sh deno add @decorators/alias ``` -[JSR][jsr] +[JSR][jsr] ```sh jsr add @decorators/alias ``` -NPM +NPM ```sh npx jsr add @decorators/alias @@ -63,26 +73,26 @@ console.assert(foo.nurp === foo.bar); // OK --- -### [`@decorators/bind`][@decorators/bind] +### [`@decorators/bind`] Bind methods, getters, and setters to the appropriate context object, with support for static members and inheritance. #### Install -Deno +Deno ```sh deno add @decorators/bind ``` -[JSR][jsr] +[JSR][jsr] ```sh jsr add @decorators/bind ``` -NPM +NPM ```sh npx jsr add @decorators/bind @@ -110,26 +120,29 @@ console.log(self === Foo); // true console.log(bar() instanceof Foo); // true ``` -### [`@decorators/types`][@decorators/types] +--- + +### [`@decorators/types`] -TypeScript type guards, function signatures, and type utilities for Stage 3 and -Legacy (Stage 2 / `experimentalDecorators`) Decorators. +Collection of type guard functions, decorator function signatures, decorator +factory signatures, and other utility types for working with both Stage 3 and +Legacy Decorators (Stage 2 / `experimentalDecorators`). #### Install -Deno +Deno ```sh deno add @decorators/types ``` -[JSR][jsr] +[JSR][jsr] ```sh jsr add @decorators/types ``` -NPM +NPM ```sh npx jsr add @decorators/types @@ -145,16 +158,18 @@ import { isLegacyDecoratorArguments, } from "@decorators/types"; -function tag( +function toStringTag( value: string, ): (...args: Args) => AnyDecoratorReturn; // deno-lint-ignore no-explicit-any -function tag(value: string): (...args: any[]) => any { +function toStringTag(value: string): (...args: any[]) => any { return (...args) => { if (isDecoratorArguments(args)) { const [target, context] = args; if (context.kind !== "class") { - throw new TypeError("@tag can only be used on classes"); + throw new TypeError( + `@toStringTag cannot decorate ${context.kind}s - it can only be used on the class itself.`, + ); } context.addInitializer(function () { Object.defineProperty(this.prototype, Symbol.toStringTag, { @@ -163,18 +178,22 @@ function tag(value: string): (...args: any[]) => any { }); }); } else if (isLegacyDecoratorArguments(args)) { - return { - [target.name]: class extends target { - get [Symbol.toStringTag](): string { - return value; - } - }, - }[target.name]; + const [target] = args; + Object.defineProperty(target.prototype, Symbol.toStringTag, { + value, + configurable: true, + }); } else { - throw new TypeError("Invalid decorator arguments"); + throw new TypeError("@toStringTag received invalid arguments"); } }; } + +// this decorator factory works in TS 4.x and 5.x without issue: +@toStringTag("Foo") +class Foo { + // ... +} ``` --- @@ -187,15 +206,14 @@ function tag(value: string): (...args: any[]) => any {
+[GitHub]: https://github.com/nberlette/decorators#readme "Check out all the '@decorators/*' packages over at the GitHub monorepo!" [@decorators/alias]: https://github.com/nberlette/decorators/tree/main/packages/alias#readme "Check out '@decorators/alias' and more over at the GitHub monorepo!" [@decorators/bind]: https://github.com/nberlette/decorators/tree/main/packages/bind#readme "Check out '@decorators/bind' and more over at the GitHub monorepo!" -[@decorators/types]: https://github.com/nberlette/decorators/tree/main/packages/tag#readme "Check out '@decorators/types' and more over at the GitHub monorepo!" -[GitHub]: https://github.com/nberlette/decorators/tree/main/packages/bind#readme "Check out all the '@decorators/*' packages over at the GitHub monorepo!" +[@decorators/types]: https://github.com/nberlette/decorators/tree/main/packages/types#readme "Check out '@decorators/types' and more over at the GitHub monorepo!" [MIT]: https://nick.mit-license.org "MIT © 2024+ Nicholas Berlette. All rights reserved." [Nicholas Berlette]: https://github.com/nberlette "Nicholas Berlette on GitHub" [Issues]: https://github.com/nberlette/decorators/issues "GitHub Issue Tracker for '@decorators/*' packages" -[Open an Issue]: https://github.com/nberlette/decorators/issues/new?assignees=nberlette&labels=bugs&title=%5Bbind%5D+ "Found a bug? Let's squash it!" -[Docs]: https://n.berlette.com/decorators "View @decorators API docs" +[Open an Issue]: https://github.com/nberlette/decorators/issues/new?assignees=nberlette&labels=bugs "Found a bug? Let's squash it!" +[Docs]: https://nberlette.github.io/decorators "View @decorators API docs" [JSR]: https://jsr.io/@decorators "View @decorators/* packages on JSR" -[Stage 3 Decorators]: https://github.com/tc39/proposal-decorators "TC39 Proposal: Decorators" [@]: https://api.iconify.design/streamline:mail-sign-at-email-at-sign-read-address.svg?width=2.5rem&height=1.4rem&color=%23fb0 From 0b8a3ad14bbd411f9e881f492ff6a1b57c81324e Mon Sep 17 00:00:00 2001 From: Nicholas Berlette Date: Sun, 19 May 2024 13:53:56 -0700 Subject: [PATCH 10/11] chore(types): remove unused import from _internal.ts --- packages/types/_internal.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/types/_internal.ts b/packages/types/_internal.ts index fed2221..e7061c9 100644 --- a/packages/types/_internal.ts +++ b/packages/types/_internal.ts @@ -15,7 +15,6 @@ import type { Constructor, Is, KeyOf, - Or, ValueOf, } from "jsr:@decorators/internal@^0.1.3"; export * from "jsr:@type/union@^0.1.0"; From 1038366e1777f0d7fe4a8db31a446db9f2a28bda Mon Sep 17 00:00:00 2001 From: Nicholas Berlette Date: Sun, 19 May 2024 14:10:35 -0700 Subject: [PATCH 11/11] fix(types): make default tuple typeparams readonly --- packages/types/signatures.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/types/signatures.ts b/packages/types/signatures.ts index f2f121a..f2f74b9 100644 --- a/packages/types/signatures.ts +++ b/packages/types/signatures.ts @@ -167,7 +167,7 @@ export interface AnyDecoratorFactory< * @category Signatures */ export interface OverloadedDecoratorFactory< - Outer extends readonly unknown[] = any[], + Outer extends readonly unknown[] = readonly any[], Kind extends DecoratorKind = DecoratorKind, Void extends VoidableArgument = true, > { @@ -195,7 +195,7 @@ export interface OverloadedDecoratorFactory< * @category Signatures */ export interface LegacyOverloadedDecoratorFactory< - Outer extends readonly unknown[] = any[], + Outer extends readonly unknown[] = readonly any[], Kind extends LegacyDecoratorKind = LegacyDecoratorKind, Void extends VoidableArgument = true, > { @@ -223,7 +223,7 @@ export interface LegacyOverloadedDecoratorFactory< * @category Signatures */ export interface AnyOverloadedDecoratorFactory< - Outer extends readonly unknown[] = any[], + Outer extends readonly unknown[] = readonly any[], Kind extends AnyDecoratorKind = AnyDecoratorKind, Void extends VoidableArgument = true, > {