I have a Logger class in typescript that takes an optional options parameter in its constructor with a class generic C that represent custom levels. This type is used within the class for methods such as log(level: Level<C>). Now, I want an updateConfig method that returns a new Logger instance where its C parameter is a union of the 'father's' custom levels + those defined in the new options object.
I have:
type DefaultLevel = 'info' | 'error'; // Default log levels
type Level<C extends string> = DefaultLevel | C
type LevelWithSilence<C extends string> = Level<C> | 'silence'
interface LoggerOptions<C extends string> {
customLevels?: Record<C, number>
level: LevelWithSilence<NoInfer<C>>
}
interface UpdatedLoggerOptions<C extends string, L extends string> {
customLevels?: Record<L, number>
level: LevelWithSilence<NoInfer<C | L >>
}
class Logger<C extends string = ''> {
options?: LoggerOptions<C>
constructor(options?: LoggerOptions<C>) {
this.options = options
}
log(level: Level<C>, message: string) {
// Log message
}
updateConfig<L extends string = never>(options?: UpdatedLoggerOptions<C, L>) {
if(!this.options) return new Logger (options as LoggerOptions<C>)
if(!options) return this
return new Logger<C | L>({
customLevels: {
...this.options.customLevels as Record<C, number>,
...options.customLevels as Record<L, number>
},
level: options.level
} as LoggerOptions<C | L>)
}
}
// Usage
const logger = new Logger({
customLevels: {
debug: 1,
trace: 2,
},
level: 'error'
});
logger.log('debug', '')
const newLogger = logger.updateConfig({
customLevels: {
test: 3,
},
level: 'test'
})
newLogger.log('debug', '') // Works
newLogger.log('test', '') // Should work
Everything appears to be typesafe apart from the log method. newLogger.log('debug', '') works, indicating that it clearly extracted the father's custom levels. On the other hand, newLogger.log('test', '') gives me error: Argument of type '"test"' is not assignable to parameter of type 'Level<"debug" | "trace">'. indicating that it failed 'extrapolating' the custom levels from the options object.
Furthermore, upon hovering over the logger object, i get as a type: const logger: Logger<"debug" | "trace">, while hovering over newLogger I get const newLogger: Logger<"debug" | "trace"> | Logger<"debug" | "trace" | "test">.
How can I achieve my desired behavior?
Given the question as currently stated, the only issue is that you need the return type of
updateConfigto beLogger<C | L>so that the returned value will accept things from bothCandL:The implementation will need type assertions or the like, so that you can convince the compiler that you are actually returning a
Logger<C | L>, since it won't be able to understand the case analysis for generics that says, for example, "ifoptionsis missing thenthisis already aLogger<C | L>because we expectLto benever." So, when necessary, double check that your assumption about the type is correct (or unlikely to be violated in practice) and then assert:Those assertions make things compile without error. Let's make sure you can use it as desired:
Looks good.
Playground link to code