¿Cómo puedo crear funciones de clase de manera programática en Typescript?
Tengo una clase de registro que necesita definir varias funciones de conveniencia como error, warn, info, etc., que todas hacen lo mismo: llamar a una función log con su nivel de severidad y los datos que se van a registrar.
Esto es verboso y desperdicia KB:
class Logger {
error(...args: any[]): void {
this.log('error', ...args)
}
warn(...args: any[]): void {
this.log('warn', ...args)
}
// ...and so on
}
Actualmente tengo un enum de todas las severidades, y una unión de cadenas de todas las severidades:
enum severityLevels {
error,
warn,
info
}
type severities = keyof typeof severityLevels
Lo que me gustaría hacer es crear programáticamente funciones para cada nivel de severidad en el constructor de la clase logger (un Object.keys(severityLevels).forEach(...) muy simple en JS puro), sin embargo no puedo averiguar cómo hacer que TypeScript permita que esto ocurra. Si hago una interfaz con todas las definiciones de funciones de severidad, no puedo implements (las funciones aún no existen). Si las hago opcionales, entonces llamar a esas funciones de registro (que "podrían no existir") se vuelve complicado.
¿Alguna idea?
Respuesta #1
Puedes agregar métodos de forma programática al prototype de un constructor en JavaScript y, por lo tanto, en TypeScript. La parte complicada es lograr que el sistema de tipos entienda lo que estás haciendo, lo cual es posible pero requiere un poco de afirmación de tipos.
Primero, vamos a crear una lista de tiempo de ejecución de esas severidades:
const severityKeys =
Object.keys(SeverityLevels).filter(k => k !== (+k) + "") as Severities[];
Nota, puse en mayúscula inicial los tipos a SeverityLevels y Severities, según la convención de TS. También nota que los objetos enum numéricos en TypeScript también tienen asignaciones inversas: Severities.error es 0, y Severities[0] es "error". Para obtener solo la lista de claves, tengo que filtrar las claves "numéricas" con k !== (+k)+"".
Luego, hagamos un ejemplo de clase BaseLogger que represente la funcionalidad de tu registrador sin los métodos añadidos:
class BaseLogger {
constructor(public loggerName: string) {
}
log(level: string, ...args: any[]) {
console.log("[" + this.loggerName + "]", "[" + level + "]", ...args);
}
}
Aquí está cómo podríamos hacer la extensión programática:
type Logger = BaseLogger & Record<Severities, (...args: any) => void>;
const Logger = (
class Logger extends BaseLogger { }
) as new (...args: ConstructorParameters<typeof BaseLogger>) => Logger;
severityKeys.forEach(k => Logger.prototype[k] = function (this: Logger, ...args: any) {
this.log(k, ...args);
})
Esto crea tanto un tipo como un valor llamado Logger, por lo que se puede usar como una class normal. El tipo corresponde al tipo de las instancias de Logger, que se parecen justo a BaseLogger con las propiedades de método añadidas con claves en Severities y valores de (...args: any[])=>void.
El valor Logger es el constructor de las instancias de Logger, que comienza como una extensión vacía de BaseLogger (aunque afirmamos que construye instancias de Logger). Después de eso, usamos severityKeys.forEach() para agregar los métodos reales a Logger.prototype según sea necesario.
Veamos si funciona:
const logger = new Logger("My Logger");
logger.error(123); // [My Logger] [error] 123
logger.info(456); // [My Logger] [info] 456
logger.warn(789); // [My Logger] [warn] 789
Me parece bien. Está bien, espero que eso ayude; ¡buena suerte!
Enlace al código
