Reputation: 1005
I have a log class that needs to define various convenience functions such as error
, warn
, info
, etc that all do the same thing: make a call to a log
function with their severity and the data to be logged.
This is verbose and wastes KBs:
class Logger {
error(...args: any[]): void {
this.log('error', ...args)
}
warn(...args: any[]): void {
this.log('warn', ...args)
}
// ...and so on
}
I currently have an enum of all the severities, and a union of strings of all the severities:
enum severityLevels {
error,
warn,
info
}
type severities = keyof typeof severityLevels
What I'd like to do is programmatically create functions for each severity level in the logger class's constructor (a very simple Object.keys(severityLevels).forEach(...)
in vanilla JS), however I can't figure out how to get TypeScript to allow this to happen. If I make an interface with all the severity function definitions, I can't implements
it (the functions don't already exist). If I make them optional, then calling those log functions (which "might not exist") gets messy.
Any ideas?
Upvotes: 3
Views: 2785
Reputation: 328262
You can programmatically add methods to a constructor's prototype
in JavaScript and therefore in TypeScript. The tricky part is getting the type system to understand what you're doing, which is doable but requires a bit of type assertion.
First, let's come up with an actual runtime list of those severities:
const severityKeys =
Object.keys(SeverityLevels).filter(k => k !== (+k) + "") as Severities[];
Note, I init-capitalized the types to SeverityLevels
and Severities
, as per TS convention. Also note that numeric enum
objects in TypeScript also have reverse mappings: Severities.error
is 0
, and Severities[0]
is "error"
. To get the list of just the keys, I have to filter out the "numeric" keys with k !== (+k)+""
.
Then, let's make an example BaseLogger
class that represents your logger's functionality without the added methods:
class BaseLogger {
constructor(public loggerName: string) {
}
log(level: string, ...args: any[]) {
console.log("[" + this.loggerName + "]", "[" + level + "]", ...args);
}
}
Here's how we might do the programmatic extension:
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);
})
This creates both a type and a value named Logger
, so it is usable like a normal class
. The type corresponds to the type of the instances of Logger
, which look just like BaseLogger
with the added method properties with keys in Severities
and values of (...args: any[])=>void
.
The value Logger
is the constructor of Logger
instances, which starts off as an empty extension of BaseLogger
(although we assert that it constructs Logger
instances). After that we use severityKeys.forEach()
to add the actual methods to Logger.prototype
as needed.
Let's see if it works:
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
Looks good to me. All right, hope that helps; good luck!
Upvotes: 8