So I have this TS code:
type DefaultLevel = 'info' | 'error'; // Default log levels
interface LoggerOptions<CustomLevels extends string, Level extends CustomLevels | DefaultLevel> {
customLevels?: Record<CustomLevels, number>
level: Level
}
class Logger<CustomLevel extends string = '', Level extends CustomLevel | DefaultLevel = CustomLevel | DefaultLevel> {
constructor(options?: LoggerOptions<CustomLevel, Level>) {
// Initialization based on options
}
log(level: Level, message: string) {
// Log message
}
}
// Usage
const logger = new Logger({
customLevels: {
debug: 1,
trace: 2,
},
level: 'debug'
});
logger.log('debug', '')
I first create a global DefaultLevel union type for my logger. Then I have a LoggerOption interface that has two generics CustomLevels and Level. CustomLevels allows me to create a custom level for my logger keeping everything type safe, while Level extends CustomLevels | DefaultLevels is needed to set the default level for my default logger. Doing level: CustomLevels did not work, hence the second generic.
My question arises in the log function of the class.
If I try to call logger.log('trace') I get a type error saying: 'Argument of type '"trace"' is not assignable to parameter of type '"debug"'.'
Hence I imagine that the LoggerOptions interface is setting the Level type value.
My question is, why within the LoggerOptions the level can be of type 'info' | 'error' | 'debug' | 'trace' (desired behavior), but later in the code, variables of type Level can only be of the string type set within the LoggerOptions?
I know that within the log() method I could do something like this:
log(level: CustomLevel | DefaultLevel, message: string) {
// Log message
}
But using the Level type would've been certainly much neater.
It doesn't look like you really want
Loggerto be generic inLevelat all. ThatLeveltype will be inferred by thelevelproperty, which will almost certainly be narrower thanCustomLevels | DefaultLevel. If you wantLevelto beCustomLevels | DefaultLevelthen use that type explicitly instead of trying to save it to another type parameter.Indeed, your inclusion of that as a type parameter seems to have been meant just as a way to prevent
CustomLevelsfrom being inferred from thelevelproperty of the constructor input. You wantto be an error, as opposed to inferring
CustomLevelsas"a" | "b" | "c". An additional type parameter can serve that purpose, but the "correct" tool for that job is to use the intrinsicNoInfer<T>utility type:Now
Loggeronly has to be generic in one type parameter:You get the error you wanted:
As well as the proper behavior for
log():Playground link to code