Возможно ли изменение типа возврата метода декоратором?
Давайте представим, что у меня есть A
декоратор, который применяется к методу, который возвращает строку. Декоратор использует эту строку и в конце возвращает B
тип (класс).
class B {
constructor(text: string) { ... }
method() {...}
}
class X {
@A
someMethod(): string {
return 'lalala';
}
}
декоратор
function A(target, property, descriptor) {
const text: string = descriptor.value();
descriptor.value = () => new B(text);
}
Что случилось? Сейчас someMethod
возвращает B
объект вместо строки. Но я не могу сделать что-то вроде этого:
class X {
constructor() {
this.someMethod().method();
}
@A
someMethod(): string {
return 'lala';
}
}
Зачем? Так как someMethod
из определения имеет строковый тип, но декоратор возвращает его B
тип. Могу ли я каким-то образом заставить машинопись узнать, что someMethod
на самом деле возвращается B
не string
?
1 ответ
Смотрите ниже для решения 3.0
Декоратор не может изменить структуру типа, никакой тип декоратора не может этого сделать (декораторы класса, метода или параметра)
Используя Typescript 2.8, вы можете написать функцию, которая примет другую функцию в качестве параметра и выполнит изменение типа возвращаемого значения, но вы потеряете такие вещи, как имена параметров, необязательные параметры и несколько подписей. Также вы должны позаботиться, так как этот способ делает это someMethod
поле, которому назначен метод, а не метод класса. Таким образом, поле должно быть назначено в каждом конструкторе, а не назначено prototype
однажды, это может повлиять на производительность.
class B<T> {
constructor(public value: T) { }
method() { return this.value; }
}
function A<T extends (...args: any[]) => any>(fn: T): ReplaceReturnType<T, B<ReturnType<T>>> {
return function (this: any, ...args: any[]) {
return new B<ReturnType<T>>(fn.apply(this, args));
} as any;
}
class X {
constructor() {
this.someMethod().method();
}
other() {}
someMethod = A(function (this: X): string {
this.other(); // We can access other members because of the explicit this parameter
return 'lala';
});
}
type IsValidArg<T> = T extends object ? keyof T extends never ? false : true : true;
type ReplaceReturnType<T, TNewReturn> = T extends (a: infer A, b: infer B, c: infer C, d: infer D, e: infer E, f: infer F, g: infer G, h: infer H, i: infer I, j: infer J) => infer R ? (
IsValidArg<J> extends true ? (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J) => TNewReturn :
IsValidArg<I> extends true ? (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I) => TNewReturn :
IsValidArg<H> extends true ? (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H) => TNewReturn :
IsValidArg<G> extends true ? (a: A, b: B, c: C, d: D, e: E, f: F, g: G) => TNewReturn :
IsValidArg<F> extends true ? (a: A, b: B, c: C, d: D, e: E, f: F) => TNewReturn :
IsValidArg<E> extends true ? (a: A, b: B, c: C, d: D, e: E) => TNewReturn :
IsValidArg<D> extends true ? (a: A, b: B, c: C, d: D) => TNewReturn :
IsValidArg<C> extends true ? (a: A, b: B, c: C) => TNewReturn :
IsValidArg<B> extends true ? (a: A, b: B) => TNewReturn :
IsValidArg<A> extends true ? (a: A) => TNewReturn :
() => TNewReturn
) : never
редактировать
Поскольку на первоначальный вопрос был дан ответ, машинопись улучшила возможное решение этой проблемы. С добавлением кортежей в параметрах отдыха и выражениях распределений нам теперь не нужно иметь все перегрузки для ReplaceReturnType
:
type ArgumentTypes<T> = T extends (... args: infer U ) => infer R ? U: never;
type ReplaceReturnType<T, TNewReturn> = (...a: ArgumentTypes<T>) => TNewReturn;
Это не только короче, но и решает ряд проблем
- Необязательные параметры остаются необязательными
- Имена аргументов сохраняются
- Работает для любого количества аргументов
Образец:
type WithOptional = ReplaceReturnType<(n?: number)=> string, Promise<string>>;
let x!: WithOptional; // Typed as (n?: number) => Promise<string>
x() // Valid
x(1); //Ok