Encountering an error due to an invalid type union when creating a new instance of a

My TypeScript code includes a Logger class that has an optional options parameter in its constructor. The options parameter includes a class generic C which represents custom levels. These custom levels are used within the class for methods like log(level: Level<C>). Now, I need to implement an updateConfig method that will return a new instance of the Logger class with the C parameter being a union of the original custom levels and those specified in the new options object.

Here's the code snippet:

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

Although everything seems to be typesafe, there is an issue with the log method. While newLogger.log('debug', '') works fine and extracts the father's custom levels correctly, attempting newLogger.log('test', '') results in the error:

Argument of type '"test"' is not assignable to parameter of type 'Level<"debug" | "trace">'.
It seems to have trouble extrapolating the custom levels from the options object.

Moreover, the type for the logger object is shown as:

const logger: Logger<"debug" | "trace">
, while for newLogger it is:
const newLogger: Logger<"debug" | "trace"> | Logger<"debug" | "trace" | "test">
.

How can I achieve the desired behavior?

Answer №1

After reviewing the question at hand, the main issue lies in the return type specification for the updateConfig function. It is crucial to define the return type as Logger<C | L> in order to accommodate values from both C and L:

updateConfig<L extends string = never>(
  options?: UpdatedLoggerOptions<C, L>
): Logger<C | L> { ⋯ }

For the implementation to work smoothly, there is a need for type assertions, or similar techniques, to convince the compiler that the return type indeed conforms to Logger<C | L>. This is due to the complexity involved in generic case analysis, where assumptions need to be verified by assertions:

updateConfig<L extends string = never>(options?: UpdatedLoggerOptions<C, L>): Logger<C | L> {

  if (!this.options) return new Logger(options as LoggerOptions<C | L>)
  // ----------------------------------------> ^^^^^^^^^^^^^^^^^^^^^^^

  if (!options) return this as Logger<C | L>
  // ---------------------> ^^^^^^^^^^^^^^^^

  return new Logger({
    customLevels: {
      ...this.options.customLevels as Record<C, number>,
      // ------------------------> ^^^^^^^^^^^^^^^^^^^^
      ...options.customLevels as Record<L, number>
      // -------------------> ^^^^^^^^^^^^^^^^^^^^
    },
    level: options.level
  })
}

By adding these assertions, the code compiles without errors. It is essential to test the functionality to ensure it works as intended:

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', '') // Functions correctly
newLogger.log('test', '') // Functions correctly

Upon testing, the code performs as expected.

Link to code on Playground

Answer №2

In a world where constructors hold privilege in the realm of Object-Oriented Programming, an interesting phenomenon occurs. When a constructor is invoked, a class is not truly instantiated. For example:

    class Foo<T extends string = '', V extends string = ''> {
       constructor(opts?: T) {}
    }
    
    // No instance of `Foo` exists, types are inferred from non-existent entity
    
    const foo = new Foo('abc');
    
    // Constructor infers type of T, but not V due to lack of information

Implementing otherwise would jeopardize type-safety, as illustrated here:


    type DefaultLevel = 'info' | 'error';
    
    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>>
    } 
    
    class Logger<C extends string = '', V extends string = ''> {
    
      constructor(options?: LoggerOptions<C>) { // Setting V without specified value
        // Initialization based on options
      }
    
      log(level: Level<C>, message: string) {
        // Log message
      }
    
      setPrimaryConfig(config: LoggerOptions<C>) {}
    
      setSecondaryConfig(config: LoggerOptions<V>) {}

}

Why is setPrimaryConfig restricted from mutating C, while V is not? Both are identical functions. Ensuring type-safety is essential to prevent type mutations in instantiated objects. Attempting to mutate generic type V on Foo<T..., V...> after construction introduces instability.

Consider this scenario:

    class Foo<T extends string = '', V extends string = ''> {
        constructor(opts?: T) {}
        private secondaryOpts?: V;
        setSecondary(opts: V) {
           this.secondaryOpts = opts;
        }
    
        get secondaryOptsOrFalse (): V extends '' ? false : V {
            return this.secondaryOpts ?? false;
        }
    
    }
    
    const foo = new Foo('abc');
    const firstForSecondaryOpts = foo.secondaryOpts // type false;
    foo.setSecondary('def');
    const secondForSecondaryOpts = foo.secondaryOpts // type 'def';

Attempting to compare the types:

    firstForSecondaryOpts === secondForSecondaryOpts

Yields a contradiction due to the disparity in types.


Furthermore, the constructor's purpose is to construct based on a predefined blueprint. Allowing other methods to override the constructor's blueprint would undermine the integrity of types in TypeScript.

Consider the implications and necessity of flexibility in types, as excessive flexibility can compromise immutability.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Issue encountered with the signature provided for a Safe API POST request

Watch this demonstration video of the issue at hand: I have created a signer using my Metamask Private Key and generated a signature from it as shown below: const signer = new ethers.Wallet(PRIVATE_KEY as string, provider) const safeInstance = new ethers. ...

Unable to retrieve the third attribute of a Class using Angular2's toString method

Here is the code snippet I am working with: import { Component } from '@angular/core'; @Component({ selector: 'my-app', template: ` <h1>Hello {{name}}</h1> <p><strong>Email:</strong> {{email}}< ...

Using TypeScript: creating functions without defining an interface

Can function props be used without an interface? I have a function with the following properties: from - HTML Element to - HTML Element coords - Array [2, 2] export const adjustElements = ({ from, to, coords }) => { let to_rect = to.getBoundingC ...

Issue with Radio Button Value Submission in Angular 6 and Laravel 5.5

I developed a CRUD application utilizing Angular and Laravel 5.5. Within this application, I included three radio buttons, but encountered an error when trying to retrieve their values... A type error occurred indicating it was unable to read the data t ...

What is the best way to implement a switch case with multiple payload types as parameters?

I am faced with the following scenario: public async handle( handler: WorkflowHandlerOption, payload: <how_to_type_it?>, ): Promise<StepResponseInterface> { switch (handler) { case WorkflowHandlerOption.JOB_APPLICATION_ACT ...

Choose only the options that are present in both arrays

I am working on creating a multiple select feature that displays all nodes, but only checks the ones that are present in 2 arrays. My front end is developed using Angular 8 and TypeScript. private mountSelect(nodesInRelation, lineApiKey) { console.lo ...

I am encountering difficulties in accessing my component's property within the corresponding template while working with Angular 5

When I call an HTTP POST method to retrieve table names from the backend, I attempt to display them in the template using ngFor. However, the table names are not appearing on the screen. The tNames property is inaccessible in the template. As a beginner i ...

Iterating through an array with ngFor to display each item based on its index

I'm working with an ngFor loop that iterates through a list of objects known as configs and displays data for each object. In addition to the configs list, I have an array in my TypeScript file that I want to include in the display. This array always ...

The issue I'm facing with Angular 8 is that while the this.router.navigate() method successfully changes the URL

As someone who is relatively new to Angular, I am currently in the process of setting up the front end of a project using Angular 8. The ultimate goal is to utilize REST API's to display data. At this point, I have developed 2 custom components. Logi ...

Exploring Vue 3: Crafting a custom plugin using the composition API and enhancing it with Typescript type augmentation

Encountering an issue with displaying plugins properly within <script> and <template> tags on WebStorm. Firstly, let's take a look at my files and configuration: tsconfig.config.json { "extends": "@vue/tsconfig/tsconfig. ...

Developing an object using class and generic features in Typescript

I am currently working on creating a function or method that can generate sorting options from an array. One example is when using Mikro-ORM, where there is a type called FindOptions<T> that can be filled with the desired sorting order for database q ...

Is it possible to use a single type predicate for multiple variables in order to achieve type inference?

Is there a way to optimize the repeated calls in this code snippet by applying a map to a type predicate so that TSC can still recognize A and B as iterables (which Sets are)? if(isSet(A) && isSet(B)) { ...

Issues encountered when attempting to use Angular2 with SystemJs and typescript transpiler

I've encountered an issue with my TypeScript transpiler setup. My @angular component isn't loading, and I'm getting this error message: ZoneAwareError: "Error: core_1.Component is not a function Evaluating http://127.0.0.1:64223/app/app.c ...

Tips for accessing and manipulating an array that is defined within a Pinia store

I have set up a store to utilize the User resource, which includes an array of roles. My goal is to search for a specific role within this array. I've attempted to use Array functions, but they are not compatible with PropType<T[]>. import route ...

Following the build process with the --prod flag in Ionic 3, users may encounter a situation where

Encountering an issue only when using --prod on Android phones. Strangely, touching anywhere triggers the event that should be fired at that specific location, causing the content to suddenly appear. I came across information suggesting a conflict between ...

What is the best method to locate an element<T> within an Array[T] when <T> is an enum?

I've recently started learning TypeScript and exploring its functionalities. I could use some assistance in deepening my understanding. Within our angular2 application, we have defined an enum for privileges as follows: export enum UserPrivileges{ ...

Typescript enhances the functionality of the Express Request body

I need help with the following code snippet: const fff = async (req: express.Request, res: express.Response): Promise<void> => {...} How can I specify that req.body.xxx exists? I want it to be recognized when I reference req.body.xxx as a propert ...

Webpack 2.7.0 throws an error: "Unexpected parameter: theme"

At the moment, I am on webpack 1.16.0 and using --theme as an argument to set the output path and plugin paths. The command appears as: rimraf dist && webpack --bail --progress --profile --theme=<name of theme> However, as I try to upgrade ...

Steps to resolve the "ERESOLVE could not resolve" error during the deployment of Firebase functions

Trying to deploy my Firebase function with the command firebase deploy --only functions:nextServer results in an error: ✔ functions: Finished running predeploy script. i functions: ensuring required API cloudfunctions.googleapis.com is enabled... i fu ...

Piping in Angular 2 with injected dependencies

Is it possible to inject dependencies such as a service into Angular 2 pipes? import {Pipe, PipeTransform} from 'angular2/core'; import {MyService} from './service'; //How can I inject MyService into the pipe? @Pipe({name: 'expo ...