typescript - ¿Cómo puedo inferir el tipo de retorno de una función decorada para usarlo en los parámetros del decorador?
Tengo un decorador que puede realizar verificaciones antes de ejecutar el método decorado. Si el método decorado devuelve un valor, quiero obligar al usuario a proporcionar un valor de retorno en el parámetro de opciones del decorador que se devolverá si una de las verificaciones falla.
Para hacer esto, me gustaría inferir el tipo de retorno del método decorado y usarlo en un tipo condicional que requiera que el parámetro de opciones incluya la propiedad returnValue con el tipo correcto si el método devuelve algo diferente de void | undefined.
Esto es lo más cerca que llegué:
function Checked<R>(
options?: [R] extends [void | undefined] ?
Options : // Derocated method doesn't return a value, so no returnValue in options
Options & OptionsWithReturnValue<R> // Derocated method returns a value, so require returnValue in options
) {
return function decorator<This extends any, Args extends any[]>(
originalMethod: (this: This, ...args: Args) => R,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => R>,
): (this: This, ...args: Args) => R {
if (options) {
return function replacement(this: This, ...args: Args): R {
if (options.checkPermissions && !checkPermissions()) {
return (options as OptionsWithReturnValue<R>).returnValue;
}
if (options.checkIsEnabled && !checkIsEnabled()) {
return (options as OptionsWithReturnValue<R>).returnValue;
}
return originalMethod.call(this, ...args);
}
}
return originalMethod;
}
}
interface Options {
checkPermissions?: boolean;
checkIsEnabled?: boolean;
}
interface OptionsWithReturnValue<R> {
returnValue: R;
}
// Dummy method for demonstration purposes
function checkPermissions() {
return Math.random() > 0.5;
}
// Dummy method for demonstration purposes
function checkIsEnabled() {
return Math.random() > 0.5;
}
class Test {
@Checked({ checkPermissions: true }) // Errors correctly
public returnsSomehing() {
return 'Hello World';
}
@Checked({ checkPermissions: true, returnValue: false }) // Errors correctly
public returnsNothing() {
console.log('Hi');
}
@Checked({ checkPermissions: true, returnValue: false }) // Should not be an error, returnValue should be boolean
public returnsBooleans() {
if (Math.random() > 0.5) {
return true;
}
return false;
}
@Checked({ checkPermissions: true, returnValue: 0 }) // Correct
public returnsNumbers() {
return Math.random();
}
@Checked({ checkPermissions: true, returnValue: 'no' }) // Should not be an error, returnValue should be string
public returnsStrings() {
if (Math.random() > 0.5) {
return 'yes';
}
return 'no';
}
}
Enlace de playground con el mismo código
Funciona más o menos en algunos casos, pero parece interpretarlo al revés. En lugar de inferir R del tipo de retorno de los métodos decorados, parece inferirlo del parámetro de opciones, si contiene la propiedad returnValue. Por ejemplo, en el caso de returnsBoolean(), R se infiere como false, pero debería ser boolean.
Me doy cuenta de que podría simplemente establecer R explícitamente, como @Checked<boolean>({ checkPermissions: true, returnValue: false }), pero no quiero eso. Quiero que el decorador sea lo más fácil y seguro de usar posible.
¿Cómo puedo hacer que el decorador infiera correctamente R a partir del tipo de retorno del método decorado?
Respuesta #1
El tipo de Checked es
declare function Checked<R>(
options?: [R] extends [void | undefined] ? Options : Options & OptionsWithReturnValue<R>
): <This extends unknown, Args extends any[]>(
originalMethod: (this: This, ...args: Args) => R,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => R>
) => (this: This, ...args: Args) => R
y cuando lo llamas como un decorador, hay múltiples sitios de inferencia para el parámetro de tipo genérico R. TypeScript a veces infiere R a partir del tipo de retorno de originalMethod como se desea, pero otras veces infiere R a partir del tipo del argumento options, lo cual no quieres.
La forma más fácil de prevenir que options se use como un sitio de inferencia es usar el tipo de utilidad NoInfer<T> allí:
declare function Checked<R>(
options?: NoInfer<
[R] extends [void | undefined] ? Options : Options & OptionsWithReturnValue<R>
>
): <This extends unknown, Args extends any[]>(
originalMethod: (this: This, ...args: Args) => R,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => R>
) => (this: This, ...args: Args) => R
interface Options {
checkPermissions?: boolean;
checkIsEnabled?: boolean;
}
Ahora, cuando llamas a Checked como un decorador, TypeScript simplemente no puede inferir R basándose en el argumento options, y en su lugar intentará inferirlo a partir de otros sitios de inferencia. Eso soluciona los problemas que estás teniendo:
// R inferred as string
@Checked({ checkPermissions: true }) // errors as desired
public returnsSomehing() {
return 'Hello World';
}
// R inferred as void
@Checked({ checkPermissions: true, returnValue: false }) // errors as desired
public returnsNothing() {
console.log('Hi');
}
// R inferred as boolean
@Checked({ checkPermissions: true, returnValue: false }) // compiles as desired
public returnsBooleans() {
if (Math.random() > 0.5) {
return true;
}
return false;
}
// R inferred as number
@Checked({ checkPermissions: true, returnValue: 0 }) // compiles as desired
public returnsNumbers() {
return Math.random();
}
// R inferred as "yes" | "no"
@Checked({ checkPermissions: true, returnValue: 'no' }) // compiles as desired
public returnsStrings() {
if (Math.random() > 0.5) {
return 'yes';
}
return 'no';
}
¡Se ve bien! Ten en cuenta que el último todavía sería un error si cambiaras returnValue a, por ejemplo, 'maybe'
// R inferred as "yes" | "no"
@Checked({ checkPermissions: true, returnValue: 'maybe' }) // error
public returnsStrings2() {
if (Math.random() > 0.5) {
return 'yes';
}
return 'no';
}
pero eso probablemente esté fuera de alcance aquí, ya que tiene que ver con la ampliación/inferencia literal, y si realmente te importara podrías anotar el tipo de retorno de la función como string para evitarlo.
Enlace de Playground al código
