In Functions, Typescript has inherited and overloaded string literal parameters

Currently, I am working on incorporating typings into a library that heavily utilizes inheritance. The hierarchy typically follows this structure:

BaseWidget --> TextBox --> ValidationTextBox

In the BaseWidget class, there is a JavaScript function called getObj(name), which translates to getObj(name: string): any in TypeScript. This function essentially searches for a method named _get<name> within the current object, executes it, and returns the result.

These 'properties' are exposed in the individual classes and inherited across classes, allowing ValidationTextBox to access all the properties present in TextBox. My question revolves around whether it is possible to add typings similar to my attempt below without having to redefine the overloads in each class.

interface BaseWidget {
    getObj(name: string): any
}

interface TextBox extends BaseWidget {
    getObj(name: "B"): string
}

interface ValidationTextBox extends TextBox {
    getObj(name: "C"): boolean
    // getObj(name: "B"): string; // Adding this line would make it compile, but it's not the best solution.
    // getObj(name: string): any // Adding this also compiles, but it results in losing type information for 'getObj("B")'
}

declare const obj: ValidationTextBox;
console.log(obj.getObj("B")); // Error: Argument of type '"B"' is not assignable to parameter of type '"C"'

TS Playground link

The current issue with this approach is the error message stating

Interface 'ValidationTextBox' incorrectly extends interface 'TextBox'
, due to the fact that "B" cannot be assigned to "C" in the context of getObj(...).

I appreciate any assistance provided!

Answer №1

There are multiple ways to tackle this issue. One approach is to satisfy the compiler by adding extra overloads without rewriting them using type queries:

interface ValidationTextBox extends TextBox {
    // Add the extra overload with 'C', along with any base class overloads (TextBox['getObj'])
    getObj: ((name: "C") => boolean) & TextBox['getObj'];
}

Another solution involves making the types generic, enabling us to enhance the type of the name parameter without directly overriding the method:

interface BaseWidget<TName = string> {
    getObj(name: TName): any
}


// Utilize TName to expand the getName overload in derived interfaces
interface TextBox<TName = never> extends BaseWidget<TName | "B"> {
}

// Utilize TName to expand the getName overload in derived interfaces
interface ValidationTextBox<TName = never> extends TextBox<TName | "C"> {
}

declare const obj: ValidationTextBox;
console.log(obj.getObj("B"));
console.log(obj.getObj("C"));

Answer №2

After experimenting with different approaches, I stumbled upon a new solution that perfectly aligns with my requirements:

// --- Definition of Properties interface to establish getObj/setObj functionality --- //
interface _Properties<T> {
    getObj<U extends keyof T>(name: U): T[U]
    setObj<U extends keyof T>(name: U, value: T[U]): void;
    setObj<U extends keyof T>(map: { [key in U]: T[key] }): void;
}

// --- Interfaces for data classes used by the TextBox classes --- //
interface SomeValue {
    a: number;
}

interface MoreSomeValue extends SomeValue {
    b: string;
}

// --- Property definitions for the classes --- //
interface TextBoxProps<ValueType = string> {
    value: ValueType; 
    disabled: boolean;
    test: SomeValue; 
}

interface ValidationTextBoxProps extends TextBoxProps<number> {
    min: number;
    max: number;
    test: MoreSomeValue
}

// --- Class interfaces extending properties --- // 
interface TextBox extends _Properties<TextBoxProps> { }

interface ValidationTextBox extends _Properties<ValidationTextBoxProps>, TextBox { }

// --- Constructor function for setting properties --- //
interface ValidationTextBoxConstructor {
    new(props: Partial<ValidationTextBoxProps>): ValidationTextBox;
}
declare const ValidationTextBoxConstructor: ValidationTextBoxConstructor;

// --- Implementation Example --- //
const obj = new ValidationTextBoxConstructor({ min: 0, max: 5, disabled: true });
console.log(obj.getObj("test"));
obj.setObj("min", 5);
obj.setObj({ min: 5, max: 0, disabled: false });

TS Playground link

The core idea behind this solution lies in the _Properties interface. By extending from it with a specified generic type T, users gain access to use getObj(name: string), where the name parameter must be a key within the provided type T and it will return the type of T[name]. Similarly,

setObj(name: string, value: T[U])
allows users to update the value of a specific key in the supplied object. The second version of setObj function also accepts a map of { key => value } pairs, looping through each entry and updating the corresponding key-value pair.

The *Props interfaces serve as containers defining the available properties for the classes, which are then used as types within the _Properties interface. They should inherit from one another if a subclass extending them is also extending _Properties.

This setup also provides a constructor function that optionally sets the property values during instantiation.

An issue arises when a property needs to undergo a drastic change in its type from the base class (e.g., switching from a string to a number in an inherited class). This challenge can be resolved through leveraging generics on the Props interface. Although not the most elegant solution, it handles rare scenarios efficiently for my current requirements.

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

Is it possible to loop through a subset of a collection using *ngFor?

Is it possible to iterate through a specific range of elements in a collection using *ngFor? For instance, I have a group of checkboxes with their form control name and label specified as follows: [{id: 'c1', label: 'C1'}, ...] Assum ...

Utilizing the power of d3.js within Angular 4

Currently, I have successfully implemented code to draw a polygon using the mouse in a normal JavaScript file. Now, I am looking to replicate the same functionality in my TypeScript file. Below is an excerpt from my d3.js file: //D3.JS VERSION 3 //------ ...

What prevents ts-morph from retrieving the classes within a TypeScript project?

Utilizing ts-morph, I am examining the inheritance relationships of classes in a project: For testing purposes, I have downloaded an open-source projectantv/x6: import { Project } from "ts-morph"; const project = new Project(); project.addDire ...

Leveraging the spread operator in cases where the value is null

Is there a more elegant solution for handling null values in the spread operator without using if-else statements? In this specific case, I only want to spread assignedStudents if it is not undefined. When attempting to do this without using if-else, I e ...

Limit the outcomes of the Ionic timepicker

Currently, I am utilizing the ionic datetime feature. However, instead of receiving just the hours, minutes, and seconds, the result I am getting looks like this 2020-10-05T00:00:27.634+07:00. What I actually require from this output is only 00:00:27. Is ...

Implementing Batch File Uploads using Typescript

Is there a way to upload multiple files in TypeScript without using React or Angular, but by utilizing an interface and getter and setter in a class? So far I have this for single file upload: <input name="myfile" type="file" multi ...

Angular Unit Test: Issue: Form control is missing a value accessor

I have structured my form field abstraction in a way that works in development but not in my unit test. // required.control.ts import { FormControl, Validators } from '@angular/forms'; export class RequiredControl extends FormControl { ...

Tips for displaying a div element alongside a button within a reactJS table

How can I position a div element next to a button in a ReactJS table? I have a table component where I want to display some options in a small window that closes when clicked outside. Options : https://i.sstatic.net/sT8Oi.png Action button in the table ...

What's the best way to add animation to the send icon while hovering over the button?

<div class="text-center"> <button [disabled]="btnStatus" class="btn btn-secondary buttonSend" type="submit"> <div [hidden]="btnStatus"> Send Message&nbsp;&nbs ...

What is the correct way to set the default function parameter as `v => v` in JavaScript?

function customFunction<T, NT extends Record<string, string | number | boolean>>( data: T, normalize?: (data: T) => NT, ) { const normalizedData = normalize ? normalize(data) : {}; return Object.keys(normalizedData); } customFuncti ...

The call does not match any overloads in Vue when using TypeScript

What is the reason behind the occurrence of the error in the ID part of the query? This call does not have a matching overload. <template> <swiper-slide slot="list" v-for="(list, index) in list.banner" :key=" ...

Tips for Customizing the Width of Your Material UI Alert Bar

At the moment, I have refrained from using any additional styling or .css files on my webpage. The width of the Alert element currently spans across the entire page. Despite my attempts to specify the width in the code snippet below, no noticeable change ...

A loop in JavaScript/TypeScript that runs precisely once every minute

Here is a snippet of my code: async run(minutesToRun: number): Promise<void> { await authenticate(); await this.stock.fillArray(); await subscribeToInstrument(this, this.orderBookId); await subscribeToOrderbook(this, this.orderBookId ...

Trying to access the 'cpf' property of an undefined value is causing an error

I have a file that I'm using to create an object to store in my Firestore database. The file is imported into my register page, and I'm using ngModels to capture user-input values. To achieve this, I've initialized my data object inside the ...

Exploring the possibilities of integrating jQuery into Angular 2 using Javascript

import {Component, ElementRef, OnInit} from 'angular2/core'; declare var jQuery:any; @Component({ selector: 'jquery-integration', templateUrl: './components/jquery-integration/jquery-integration.html' } ...

I am having trouble locating my source code within the Typescript module

My issue lies with a file called Server.js, which holds the code for export class Program and includes <reference path='mscorlib.ts'/>. However, when I compile it using the command: tsc -t ES5 Server.ts --module commonjs --out Server.js T ...

Generate incorrect JSON for a negative test in TypeScript

Struggling with navigating TS (amazing) static typing to generate invalid data for a negative test scenario. I am currently working on resolving an issue where the client may pass invalid JSON (double quotes within the value). I believe I have fixed the i ...

Is Angular's ngOnChanges failing to detect any changes?

Within one component, I have a dropdown list. Whenever the value of the dropdown changes, I am attempting to detect this change in value in another component. However, I am encountering an unusual issue. Sometimes, changing the dropdown value triggers the ...

Having trouble getting the Bootstrap 5 Modal to function properly within an Electron app

Facing an issue with my web app using Bootstrap 5, as the modal is not displaying properly. Below is the HTML code for the modal and the button that triggers it: <div class="modal" tabindex="-1" id=&quo ...

What could be causing NestJS/TypeORM to remove the attribute passed in during save operation?

Embarking on my Nest JS journey, I set up my first project to familiarize myself with it. Despite successfully working with the Organization entity, I encountered a roadblock when trying to create a User - organizationId IS NULL and cannot be saved. Here ...