Skip to content

Instantly share code, notes, and snippets.

@rbuckton
Last active November 27, 2022 10:23
Show Gist options
  • Save rbuckton/a464d1a0997bd3dab36c8b0caef0959a to your computer and use it in GitHub Desktop.
Save rbuckton/a464d1a0997bd3dab36c8b0caef0959a to your computer and use it in GitHub Desktop.
export function createDecoratorAdapter() {
type LegacyClassDecorator = <T extends abstract new (...args: any) => any>(target: T) => T | void;
type LegacyMemberDecorator = <T>(target: unknown, key: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
type LegacyParameterDecorator = (target: unknown, key: string | symbol, index: number) => void;
type LegacyClassDecoratorConstraint<TDecorator extends LegacyClassDecorator> =
TDecorator extends <T extends infer U extends abstract new (...args: any) => any>(target: T) => T | void ? U :
(abstract new (...args: any) => any);
type LegacyMemberDecoratorConstraint<TDecorator extends LegacyMemberDecorator> =
TDecorator extends <T extends infer U>(target: any, key: any, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void ? U :
unknown;
interface DecoratedElement {
context: DecoratorContext;
decorators: ((...args: any) => any)[];
params?: boolean;
}
let applied = false;
let lastElement: DecoratedElement | undefined;
const elements: DecoratedElement[] = [];
adapter.params = params;
return adapter;
function adapter<T extends abstract new (...args: any) => any>(target: T, context: ClassDecoratorContext): T | void;
// class decorator adapter
function adapter<TDecorator extends LegacyClassDecorator>(decorator: TDecorator): {
<T extends LegacyClassDecoratorConstraint<TDecorator>>(target: T, context: ClassDecoratorContext): void;
};
// member decorator adapter
function adapter<TDecorator extends LegacyMemberDecorator>(decorator: TDecorator): {
<T extends LegacyMemberDecoratorConstraint<TDecorator>>(target: T, context: ClassMethodDecoratorContext): void;
<T extends LegacyMemberDecoratorConstraint<TDecorator>>(target: () => T, context: ClassGetterDecoratorContext): void;
<T extends LegacyMemberDecoratorConstraint<TDecorator>>(target: (value: T) => void, context: ClassSetterDecoratorContext): void;
<T extends LegacyMemberDecoratorConstraint<TDecorator>>(target: ClassAccessorDecoratorTarget<unknown, T>, context: ClassAccessorDecoratorContext): void;
(target: undefined, context: ClassFieldDecoratorContext): void;
};
function adapter(decorator: any, context?: ClassDecoratorContext): any {
if (context?.kind === "class") {
applyElements(decorator);
return;
}
if (context) {
throw new TypeError("Invalid arguments");
}
if (applied) throw new TypeError("Invalid attempt to adapt a decorator in an already-applied adapter.");
return function (target: any, context: DecoratorContext) {
if (context.kind === "class") {
target = applyElements(target);
return decorator(target);
}
if (applied) throw new TypeError("Invalid attempt to adapt a decorator in an already-applied adapter.");
if (context.private) throw new TypeError("Legacy decorators are not supported on private members");
if (lastElement === undefined || !isMatchingContext(lastElement.context, context)) {
lastElement = { context, decorators: [] };
elements.push(lastElement);
}
if (lastElement.context.kind !== context.kind) throw new TypeError("Legacy decorators cannot decorate both the getter and the setter.");
lastElement.decorators.push(decorator);
};
}
function params(parameters: (LegacyParameterDecorator[] | undefined)[]) {
if (applied) throw new TypeError("Invalid attempt to adapt a decorator in an already-applied adapter.");
return function (target_: unknown, context: DecoratorContext) {
if (applied) throw new TypeError("Invalid attempt to adapt a decorator in an already-applied adapter.");
if (context.kind !== "class" && context.private) throw new TypeError("Legacy decorators are not supported on private members");
if (lastElement === undefined || !isMatchingContext(lastElement.context, context)) {
lastElement = { context, decorators: [] };
elements.push(lastElement);
}
if (lastElement.context.kind !== context.kind) throw new TypeError("Legacy decorators cannot decorate both the getter and the setter.");
if (lastElement.params) throw new TypeError("Parameter decorators already defined.");
if (lastElement.decorators.length) throw new TypeError("Parameter decorators must be the closest decorator to the decorated element.");
lastElement.params = true;
for (let i = 0; i < parameters.length; i++) {
const decorators = parameters[i];
if (!decorators) continue;
for (let j = decorators.length - 1; j >= 0; j--) {
const decorator = decorators[j];
lastElement.decorators.push((target, key) => { decorator(target, key, i); });
}
}
};
}
function applyElements(target: abstract new (...args: any[]) => any) {
applied = true;
for (const element of elements) {
switch (element.context.kind) {
case "method":
case "getter":
case "setter":
case "accessor": {
const legacyTarget = element.context.static ? target : target.prototype;
let descriptor = Object.getOwnPropertyDescriptor(legacyTarget, element.context.name) ?? {};
for (const decorator of element.decorators) {
descriptor = decorator(legacyTarget, element.context.name, { ...descriptor }) ?? descriptor;
}
Object.defineProperty(legacyTarget, element.context.name, descriptor);
break;
}
case "field":
const legacyTarget = element.context.static ? target : target.prototype;
for (const decorator of element.decorators) {
decorator(legacyTarget, element.context.name);
}
break;
case "class":
// these should only exist for `.params()` elements.
for (const decorator of element.decorators) {
target = decorator(target) ?? target;
}
break;
}
}
elements.length = 0;
return target;
}
function isMatchingContext(a: DecoratorContext, b: DecoratorContext) {
return a.kind === "class" ? b.kind === "class" :
b.kind !== "class" && a.name === b.name && a.static === b.static && b.private === false;
}
}
// examples
function legacyClassDecorator<T extends abstract new (...args: any) => any>(target: T) {
abstract class subtype extends target {
constructor(...args: any) {
super(...args);
console.log("subtype");
}
}
return subtype as T;
}
function legacyMemberDecorator<T>(target: unknown, key: string | symbol, descriptor: TypedPropertyDescriptor<T>) {
console.log("member", key);
}
function legacyParameterDecorator(target: unknown, key: string | symbol | unknown, parameterIndex: number) {
console.log("parameter", key, parameterIndex);
}
function legacyFieldDecorator(target: unknown, key: string | symbol) {
console.log("field", key);
}
let _ = createDecoratorAdapter();
@_(legacyClassDecorator)
class C {
@_(legacyMemberDecorator)
@_.params([ [legacyParameterDecorator] ])
method(x: number) {}
@_(legacyFieldDecorator) field = 1;
}
new C();
// or, if you don't have a class decorator
_ = createDecoratorAdapter(); // reset the adapter for a new class
@_
class D {
@_(legacyMemberDecorator)
@_.params([ [legacyParameterDecorator] ])
method(x: number) {}
@_(legacyFieldDecorator) field = 1;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment